Compare commits
11 Commits
hotfix/fet
...
test/backe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c188f865a2 | ||
|
|
a2a675dd86 | ||
|
|
b867ba9139 | ||
|
|
d2ca631492 | ||
|
|
39a4fd8ab2 | ||
|
|
76d52a3b05 | ||
|
|
b83cc38a1c | ||
|
|
db42073d42 | ||
|
|
6c928e72d4 | ||
|
|
ddd0a67da3 | ||
|
|
295304feeb |
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
**/*/node_modules
|
|
||||||
@@ -13,7 +13,6 @@ SESSION_SECRET='add some secret here'
|
|||||||
# Hoppscotch App Domain Config
|
# Hoppscotch App Domain Config
|
||||||
REDIRECT_URL="http://localhost:3000"
|
REDIRECT_URL="http://localhost:3000"
|
||||||
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
|
||||||
|
|
||||||
# Google Auth Config
|
# Google Auth Config
|
||||||
GOOGLE_CLIENT_ID="************************************************"
|
GOOGLE_CLIENT_ID="************************************************"
|
||||||
|
|||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -2,9 +2,9 @@ name: Node.js CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, staging, "release/**"]
|
branches: [main, staging]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, staging, "release/**"]
|
branches: [main, staging]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
semi: false,
|
semi: false
|
||||||
trailingComma: "es5",
|
|
||||||
singleQuote: false,
|
|
||||||
printWidth: 80,
|
|
||||||
useTabs: false,
|
|
||||||
tabWidth: 2
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
:3000 {
|
|
||||||
try_files {path} /
|
|
||||||
root * /site/selfhost-web
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
:3100 {
|
|
||||||
try_files {path} /
|
|
||||||
root * /site/sh-admin
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
72
aio_run.mjs
72
aio_run.mjs
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/local/bin/node
|
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
import { execSync, spawn } from "child_process"
|
|
||||||
import fs from "fs"
|
|
||||||
import process from "process"
|
|
||||||
|
|
||||||
function runChildProcessWithPrefix(command, args, prefix) {
|
|
||||||
const childProcess = spawn(command, args);
|
|
||||||
|
|
||||||
childProcess.stdout.on('data', (data) => {
|
|
||||||
const output = data.toString().trim().split('\n');
|
|
||||||
output.forEach((line) => {
|
|
||||||
console.log(`${prefix} | ${line}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.stderr.on('data', (data) => {
|
|
||||||
const error = data.toString().trim().split('\n');
|
|
||||||
error.forEach((line) => {
|
|
||||||
console.error(`${prefix} | ${line}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.on('close', (code) => {
|
|
||||||
console.log(`${prefix} Child process exited with code ${code}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.on('error', (stuff) => {
|
|
||||||
console.log("error")
|
|
||||||
console.log(stuff)
|
|
||||||
})
|
|
||||||
|
|
||||||
return childProcess
|
|
||||||
}
|
|
||||||
|
|
||||||
const envFileContent = Object.entries(process.env)
|
|
||||||
.filter(([env]) => env.startsWith("VITE_"))
|
|
||||||
.map(([env, val]) => `${env}=${
|
|
||||||
(val.startsWith("\"") && val.endsWith("\""))
|
|
||||||
? val
|
|
||||||
: `"${val}"`
|
|
||||||
}`)
|
|
||||||
.join("\n")
|
|
||||||
|
|
||||||
fs.writeFileSync("build.env", envFileContent)
|
|
||||||
|
|
||||||
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
|
|
||||||
|
|
||||||
fs.rmSync("build.env")
|
|
||||||
|
|
||||||
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
|
|
||||||
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
|
|
||||||
|
|
||||||
caddyProcess.on("exit", (code) => {
|
|
||||||
console.log(`Exiting process because Caddy Server exited with code ${code}`)
|
|
||||||
process.exit(code)
|
|
||||||
})
|
|
||||||
|
|
||||||
backendProcess.on("exit", (code) => {
|
|
||||||
console.log(`Exiting process because Backend Server exited with code ${code}`)
|
|
||||||
process.exit(code)
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log("SIGINT received, exiting...")
|
|
||||||
|
|
||||||
caddyProcess.kill("SIGINT")
|
|
||||||
backendProcess.kill("SIGINT")
|
|
||||||
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
@@ -8,25 +8,23 @@ services:
|
|||||||
hoppscotch-backend:
|
hoppscotch-backend:
|
||||||
container_name: hoppscotch-backend
|
container_name: hoppscotch-backend
|
||||||
build:
|
build:
|
||||||
dockerfile: prod.Dockerfile
|
dockerfile: packages/hoppscotch-backend/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
target: backend
|
target: prod
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
||||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
- PORT=3170
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
- ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
|
||||||
- /usr/src/app/node_modules/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
hoppscotch-db:
|
- hoppscotch-db
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
ports:
|
||||||
- "3170:3170"
|
- "3170:3000"
|
||||||
|
|
||||||
# The main hoppscotch app. This will be hosted at port 3000
|
# The main hoppscotch app. This will be hosted at port 3000
|
||||||
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
||||||
@@ -34,9 +32,8 @@ services:
|
|||||||
hoppscotch-app:
|
hoppscotch-app:
|
||||||
container_name: hoppscotch-app
|
container_name: hoppscotch-app
|
||||||
build:
|
build:
|
||||||
dockerfile: prod.Dockerfile
|
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
target: app
|
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -50,9 +47,8 @@ services:
|
|||||||
hoppscotch-sh-admin:
|
hoppscotch-sh-admin:
|
||||||
container_name: hoppscotch-sh-admin
|
container_name: hoppscotch-sh-admin
|
||||||
build:
|
build:
|
||||||
dockerfile: prod.Dockerfile
|
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
target: sh_admin
|
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -60,91 +56,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3100:8080"
|
- "3100:8080"
|
||||||
|
|
||||||
# The service that spins up all 3 services at once in one container
|
|
||||||
hoppscotch-aio:
|
|
||||||
container_name: hoppscotch-aio
|
|
||||||
build:
|
|
||||||
dockerfile: prod.Dockerfile
|
|
||||||
context: .
|
|
||||||
target: aio
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
depends_on:
|
|
||||||
hoppscotch-db:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
- "3100:3100"
|
|
||||||
- "3170:3170"
|
|
||||||
|
|
||||||
# The preset DB service, you can delete/comment the below lines if
|
# The preset DB service, you can delete/comment the below lines if
|
||||||
# you are using an external postgres instance
|
# you are using an external postgres instance
|
||||||
# This will be exposed at port 5432
|
# This will be exposed at port 5432
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
image: postgres:15
|
image: postgres
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
user: postgres
|
|
||||||
environment:
|
environment:
|
||||||
# The default user defined by the docker image
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
# NOTE: Please UPDATE THIS PASSWORD!
|
# NOTE: Please UPDATE THIS PASSWORD!
|
||||||
POSTGRES_PASSWORD: testpass
|
POSTGRES_PASSWORD: testpass
|
||||||
POSTGRES_DB: hoppscotch
|
POSTGRES_DB: hoppscotch
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
|
|
||||||
]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
# All the services listed below are deprececated
|
|
||||||
hoppscotch-old-backend:
|
|
||||||
container_name: hoppscotch-old-backend
|
|
||||||
build:
|
|
||||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
|
||||||
context: .
|
|
||||||
target: prod
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
|
||||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
|
||||||
- PORT=3000
|
|
||||||
volumes:
|
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
|
||||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
|
||||||
- /usr/src/app/node_modules/
|
|
||||||
depends_on:
|
|
||||||
hoppscotch-db:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "3170:3000"
|
|
||||||
|
|
||||||
hoppscotch-old-app:
|
|
||||||
container_name: hoppscotch-old-app
|
|
||||||
build:
|
|
||||||
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
|
||||||
context: .
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
depends_on:
|
|
||||||
- hoppscotch-old-backend
|
|
||||||
ports:
|
|
||||||
- "3000:8080"
|
|
||||||
|
|
||||||
hoppscotch-old-sh-admin:
|
|
||||||
container_name: hoppscotch-old-sh-admin
|
|
||||||
build:
|
|
||||||
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
|
||||||
context: .
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
depends_on:
|
|
||||||
- hoppscotch-old-backend
|
|
||||||
ports:
|
|
||||||
- "3100:8080"
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
curlCheck() {
|
|
||||||
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
|
|
||||||
echo "URL request failed!"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "URL request succeeded!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
curlCheck "http://localhost:3000"
|
|
||||||
curlCheck "http://localhost:3100"
|
|
||||||
curlCheck "http://localhost:3170/ping"
|
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.9.0",
|
"@codemirror/language": "^6.2.0",
|
||||||
"@lezer/highlight": "^1.1.6",
|
"@lezer/highlight": "^1.0.0",
|
||||||
"@lezer/lr": "^1.3.10"
|
"@lezer/lr": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.5.0",
|
"@lezer/generator": "^1.1.0",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"rollup": "^2.70.2",
|
"rollup": "^2.70.2",
|
||||||
"rollup-plugin-dts": "^4.2.1",
|
"rollup-plugin-dts": "^4.2.1",
|
||||||
|
|||||||
24
packages/dioc/.gitignore
vendored
24
packages/dioc/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
# dioc
|
|
||||||
|
|
||||||
A small and lightweight dependency injection / inversion of control system.
|
|
||||||
|
|
||||||
### About
|
|
||||||
|
|
||||||
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
|
|
||||||
|
|
||||||
### Demo
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Service, Container } from "dioc"
|
|
||||||
|
|
||||||
// Here is a simple service, which you can define by extending the Service class
|
|
||||||
// and providing an ID static field (of type string)
|
|
||||||
export class PersistenceService extends Service {
|
|
||||||
// This should be unique for each container
|
|
||||||
public static ID = "PERSISTENCE_SERVICE"
|
|
||||||
|
|
||||||
public read(key: string): string | undefined {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
public write(key: string, value: string) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TodoServiceEvent =
|
|
||||||
| { type: "TODO_CREATED"; index: number }
|
|
||||||
| { type: "TODO_DELETED"; index: number }
|
|
||||||
|
|
||||||
// Services have a built in event system
|
|
||||||
// Define the generic argument to say what are the possible emitted values
|
|
||||||
export class TodoService extends Service<TodoServiceEvent> {
|
|
||||||
public static ID = "TODO_SERVICE"
|
|
||||||
|
|
||||||
// Inject persistence service into this service
|
|
||||||
private readonly persistence = this.bind(PersistenceService)
|
|
||||||
|
|
||||||
public todos = []
|
|
||||||
|
|
||||||
// Service constructors cannot have arguments
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
|
|
||||||
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
|
|
||||||
}
|
|
||||||
|
|
||||||
public addTodo(text: string) {
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// You can access services via the bound fields
|
|
||||||
this.persistence.write("todos", JSON.stringify(this.todos))
|
|
||||||
|
|
||||||
// This is how you emit an event
|
|
||||||
this.emit({
|
|
||||||
type: "TODO_CREATED",
|
|
||||||
index,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeTodo(index: number) {
|
|
||||||
// ...
|
|
||||||
|
|
||||||
this.emit({
|
|
||||||
type: "TODO_DELETED",
|
|
||||||
index,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Services need a container to run in
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
// You can initialize and get services using Container#bind
|
|
||||||
// It will automatically initialize the service (and its dependencies)
|
|
||||||
const todoService = container.bind(TodoService) // Returns an instance of TodoService
|
|
||||||
```
|
|
||||||
|
|
||||||
### Demo (Unit Test)
|
|
||||||
|
|
||||||
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { TestContainer } from "dioc/testing"
|
|
||||||
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
|
|
||||||
import { describe, it, expect, vi } from "vitest"
|
|
||||||
|
|
||||||
describe("TodoService", () => {
|
|
||||||
it("addTodo writes to persistence", () => {
|
|
||||||
const container = new TestContainer()
|
|
||||||
|
|
||||||
const writeFn = vi.fn()
|
|
||||||
|
|
||||||
// The first parameter is the service to mock and the second parameter
|
|
||||||
// is the mocked service fields and functions
|
|
||||||
container.bindMock(PersistenceService, {
|
|
||||||
read: () => undefined, // Not really important for this test
|
|
||||||
write: writeFn,
|
|
||||||
})
|
|
||||||
|
|
||||||
// the peristence service bind in TodoService will now use the
|
|
||||||
// above defined mocked implementation
|
|
||||||
const todoService = container.bind(TodoService)
|
|
||||||
|
|
||||||
todoService.addTodo("sup")
|
|
||||||
|
|
||||||
expect(writeFn).toHaveBeenCalledOnce()
|
|
||||||
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Demo (Vue)
|
|
||||||
|
|
||||||
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
|
|
||||||
|
|
||||||
In the app entry point:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { createApp } from "vue"
|
|
||||||
import { diocPlugin } from "dioc/vue"
|
|
||||||
|
|
||||||
const app = createApp()
|
|
||||||
|
|
||||||
app.use(diocPlugin, {
|
|
||||||
container: new Container(), // You can pass in the container you want to provide to the components here
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
In your Vue components:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup>
|
|
||||||
import { TodoService } from "./demo.ts" // The above demo
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
|
|
||||||
const todoService = useService(TodoService) // Returns an instance of the TodoService class
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
2
packages/dioc/index.d.ts
vendored
2
packages/dioc/index.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./dist/main.d.ts"
|
|
||||||
export * from "./dist/main.d.ts"
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import { Service } from "./service"
|
|
||||||
import { Observable, Subject } from 'rxjs'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the current container instance in the current operating context.
|
|
||||||
*
|
|
||||||
* NOTE: This should not be used outside of dioc library code
|
|
||||||
*/
|
|
||||||
export let currentContainer: Container | null = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The events emitted by the container
|
|
||||||
*
|
|
||||||
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
|
|
||||||
* `SERVICE_INIT` - emitted when a service is initialized
|
|
||||||
*/
|
|
||||||
export type ContainerEvent =
|
|
||||||
| {
|
|
||||||
type: 'SERVICE_BIND';
|
|
||||||
|
|
||||||
/** The Service ID of the service being bounded (the dependency) */
|
|
||||||
boundeeID: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Service ID of the bounder that is binding the boundee (the dependent)
|
|
||||||
*
|
|
||||||
* NOTE: This will be undefined if the service is bound directly to the container
|
|
||||||
*/
|
|
||||||
bounderID: string | undefined
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'SERVICE_INIT';
|
|
||||||
|
|
||||||
/** The Service ID of the service being initialized */
|
|
||||||
serviceID: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
|
|
||||||
*/
|
|
||||||
export class Container {
|
|
||||||
/** Used during the `bind` operation to detect circular dependencies */
|
|
||||||
private bindStack: string[] = []
|
|
||||||
|
|
||||||
/** The map of bound services to their IDs */
|
|
||||||
protected boundMap = new Map<string, Service<unknown>>()
|
|
||||||
|
|
||||||
/** The RxJS observable representing the event stream */
|
|
||||||
protected event$ = new Subject<ContainerEvent>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether a container has the given service bound
|
|
||||||
* @param service The service to check for
|
|
||||||
*/
|
|
||||||
public hasBound<
|
|
||||||
T extends typeof Service<any> & { ID: string }
|
|
||||||
>(service: T): boolean {
|
|
||||||
return this.boundMap.has(service.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the service bound to the container with the given ID or if not found, undefined.
|
|
||||||
*
|
|
||||||
* NOTE: This is an advanced method and should not be used as much as possible.
|
|
||||||
*
|
|
||||||
* @param serviceID The ID of the service to get
|
|
||||||
*/
|
|
||||||
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
|
|
||||||
return this.boundMap.get(serviceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a service to the container. This is equivalent to marking a service as a dependency.
|
|
||||||
* @param service The class reference of a service to bind
|
|
||||||
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
|
|
||||||
*/
|
|
||||||
public bind<T extends typeof Service<any> & { ID: string }>(
|
|
||||||
service: T,
|
|
||||||
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
|
|
||||||
): InstanceType<T> {
|
|
||||||
// We need to store the current container in a variable so that we can restore it after the bind operation
|
|
||||||
const oldCurrentContainer = currentContainer;
|
|
||||||
currentContainer = this;
|
|
||||||
|
|
||||||
// If the service is already bound, return the existing instance
|
|
||||||
if (this.hasBound(service)) {
|
|
||||||
this.event$.next({
|
|
||||||
type: 'SERVICE_BIND',
|
|
||||||
boundeeID: service.ID,
|
|
||||||
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect circular dependency and throw error
|
|
||||||
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
|
|
||||||
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
|
|
||||||
|
|
||||||
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the service ID onto the bind stack to detect circular dependencies
|
|
||||||
this.bindStack.push(service.ID)
|
|
||||||
|
|
||||||
// Initialize the service and emit events
|
|
||||||
|
|
||||||
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
|
|
||||||
const instance: Service<any> = new (service as any)()
|
|
||||||
|
|
||||||
this.boundMap.set(service.ID, instance)
|
|
||||||
|
|
||||||
this.bindStack.pop()
|
|
||||||
|
|
||||||
this.event$.next({
|
|
||||||
type: 'SERVICE_INIT',
|
|
||||||
serviceID: service.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.event$.next({
|
|
||||||
type: 'SERVICE_BIND',
|
|
||||||
boundeeID: service.ID,
|
|
||||||
bounderID: bounder?.ID
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// Restore the current container
|
|
||||||
currentContainer = oldCurrentContainer;
|
|
||||||
|
|
||||||
// We expect the return type to match the service definition
|
|
||||||
return instance as InstanceType<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an iterator of the currently bound service IDs and their instances
|
|
||||||
*/
|
|
||||||
public getBoundServices(): IterableIterator<[string, Service<any>]> {
|
|
||||||
return this.boundMap.entries()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the public container event stream
|
|
||||||
*/
|
|
||||||
public getEventStream(): Observable<ContainerEvent> {
|
|
||||||
return this.event$.asObservable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./container"
|
|
||||||
export * from "./service"
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { Observable, Subject } from 'rxjs'
|
|
||||||
import { Container, currentContainer } from './container'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Dioc service that can bound to a container and can bind dependency services.
|
|
||||||
*
|
|
||||||
* NOTE: Services cannot have a constructor that takes arguments.
|
|
||||||
*
|
|
||||||
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
|
|
||||||
*/
|
|
||||||
export abstract class Service<EventDef = {}> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The internal event stream of the service
|
|
||||||
*/
|
|
||||||
private event$ = new Subject<EventDef>()
|
|
||||||
|
|
||||||
/** The container the service is bound to */
|
|
||||||
#container: Container
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (!currentContainer) {
|
|
||||||
throw new Error(
|
|
||||||
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#container = currentContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a dependency service into this service.
|
|
||||||
* @param service The class reference of the service to bind
|
|
||||||
*/
|
|
||||||
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
|
|
||||||
if (!currentContainer) {
|
|
||||||
throw new Error('No currentContainer defined.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the container the service is bound to
|
|
||||||
*/
|
|
||||||
protected getContainer(): Container {
|
|
||||||
return this.#container
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits an event on the service's event stream
|
|
||||||
* @param event The event to emit
|
|
||||||
*/
|
|
||||||
protected emit(event: EventDef) {
|
|
||||||
this.event$.next(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the event stream of the service
|
|
||||||
*/
|
|
||||||
public getEventStream(): Observable<EventDef> {
|
|
||||||
|
|
||||||
return this.event$.asObservable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Container, Service } from "./main";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A container that can be used for writing tests, contains additional methods
|
|
||||||
* for binding suitable for writing tests. (see `bindMock`).
|
|
||||||
*/
|
|
||||||
export class TestContainer extends Container {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a mock service to the container.
|
|
||||||
*
|
|
||||||
* @param service
|
|
||||||
* @param mock
|
|
||||||
*/
|
|
||||||
public bindMock<
|
|
||||||
T extends typeof Service<any> & { ID: string },
|
|
||||||
U extends Partial<InstanceType<T>>
|
|
||||||
>(service: T, mock: U): U {
|
|
||||||
if (this.boundMap.has(service.ID)) {
|
|
||||||
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.boundMap.set(service.ID, mock as any)
|
|
||||||
|
|
||||||
this.event$.next({
|
|
||||||
type: "SERVICE_BIND",
|
|
||||||
boundeeID: service.ID,
|
|
||||||
bounderID: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Plugin, inject } from "vue"
|
|
||||||
import { Container } from "./container"
|
|
||||||
import { Service } from "./service"
|
|
||||||
|
|
||||||
const VUE_CONTAINER_KEY = Symbol()
|
|
||||||
|
|
||||||
// TODO: Some Vue version issue with plugin generics is breaking type checking
|
|
||||||
/**
|
|
||||||
* The Vue Dioc Plugin, this allows the composables to work and access the container
|
|
||||||
*
|
|
||||||
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
|
|
||||||
*/
|
|
||||||
export const diocPlugin: Plugin = {
|
|
||||||
install(app, { container }) {
|
|
||||||
app.provide(VUE_CONTAINER_KEY, container)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A composable that binds a service to a Vue Component
|
|
||||||
*
|
|
||||||
* @param service The class reference of the service to bind
|
|
||||||
*/
|
|
||||||
export function useService<
|
|
||||||
T extends typeof Service<any> & { ID: string }
|
|
||||||
>(service: T): InstanceType<T> {
|
|
||||||
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
throw new Error("Container not found, did you forget to install the dioc plugin?")
|
|
||||||
}
|
|
||||||
|
|
||||||
return container.bind(service)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "dioc",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
|
||||||
"type": "module",
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"index.d.ts"
|
|
||||||
],
|
|
||||||
"main": "./dist/counter.umd.cjs",
|
|
||||||
"module": "./dist/counter.js",
|
|
||||||
"types": "./index.d.ts",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/main.d.ts",
|
|
||||||
"require": "./dist/index.cjs",
|
|
||||||
"import": "./dist/index.js"
|
|
||||||
},
|
|
||||||
"./vue": {
|
|
||||||
"types": "./dist/vue.d.ts",
|
|
||||||
"require": "./dist/vue.cjs",
|
|
||||||
"import": "./dist/vue.js"
|
|
||||||
},
|
|
||||||
"./testing": {
|
|
||||||
"types": "./dist/testing.d.ts",
|
|
||||||
"require": "./dist/testing.cjs",
|
|
||||||
"import": "./dist/testing.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build && tsc --emitDeclarationOnly",
|
|
||||||
"prepare": "pnpm run build",
|
|
||||||
"test": "vitest run",
|
|
||||||
"do-test": "pnpm run test",
|
|
||||||
"test:watch": "vitest"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^4.9.4",
|
|
||||||
"vite": "^4.0.4",
|
|
||||||
"vitest": "^0.29.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"rxjs": "^7.8.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.2.25"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"vue": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
import { it, expect, describe, vi } from "vitest"
|
|
||||||
import { Service } from "../lib/service"
|
|
||||||
import { Container, currentContainer, ContainerEvent } from "../lib/container"
|
|
||||||
|
|
||||||
class TestServiceA extends Service {
|
|
||||||
public static ID = "TestServiceA"
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestServiceB extends Service {
|
|
||||||
public static ID = "TestServiceB"
|
|
||||||
|
|
||||||
// Marked public to allow for testing
|
|
||||||
public readonly serviceA = this.bind(TestServiceA)
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Container", () => {
|
|
||||||
describe("getBoundServiceWithID", () => {
|
|
||||||
it("returns the service instance if it is bound to the container", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const service = container.bind(TestServiceA)
|
|
||||||
|
|
||||||
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns undefined if the service is not bound to the container", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("bind", () => {
|
|
||||||
it("correctly binds the service to it", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const service = container.bind(TestServiceA)
|
|
||||||
|
|
||||||
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
|
||||||
expect(service.getContainer()).toBe(container)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("after bind, the current container is set back to its previous value", () => {
|
|
||||||
const originalValue = currentContainer
|
|
||||||
|
|
||||||
const container = new Container()
|
|
||||||
container.bind(TestServiceA)
|
|
||||||
|
|
||||||
expect(currentContainer).toBe(originalValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("dependent services are registered in the same container", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceB = container.bind(TestServiceB)
|
|
||||||
|
|
||||||
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
|
||||||
expect(serviceB.serviceA.getContainer()).toBe(container)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceA = container.bind(TestServiceA)
|
|
||||||
const serviceA2 = container.bind(TestServiceA)
|
|
||||||
|
|
||||||
expect(serviceA).toBe(serviceA2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceB = container.bind(TestServiceB)
|
|
||||||
const serviceA = container.bind(TestServiceA)
|
|
||||||
|
|
||||||
expect(serviceB.serviceA).toBe(serviceA)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("binding an initialized service as a dependency returns the same instance", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceA = container.bind(TestServiceA)
|
|
||||||
const serviceB = container.bind(TestServiceB)
|
|
||||||
|
|
||||||
expect(serviceB.serviceA).toBe(serviceA)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceFunc = vi.fn<
|
|
||||||
[ContainerEvent & { type: "SERVICE_INIT" }],
|
|
||||||
void
|
|
||||||
>()
|
|
||||||
|
|
||||||
container.getEventStream().subscribe((ev) => {
|
|
||||||
if (ev.type === "SERVICE_INIT") {
|
|
||||||
serviceFunc(ev)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const instance = container.bind(TestServiceA)
|
|
||||||
|
|
||||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
|
||||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
|
||||||
type: "SERVICE_INIT",
|
|
||||||
serviceID: TestServiceA.ID,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceFunc = vi.fn<
|
|
||||||
[ContainerEvent & { type: "SERVICE_BIND" }],
|
|
||||||
void
|
|
||||||
>()
|
|
||||||
|
|
||||||
container.getEventStream().subscribe((ev) => {
|
|
||||||
if (ev.type === "SERVICE_BIND") {
|
|
||||||
serviceFunc(ev)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
container.bind(TestServiceA)
|
|
||||||
|
|
||||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
|
||||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
|
||||||
type: "SERVICE_BIND",
|
|
||||||
boundeeID: TestServiceA.ID,
|
|
||||||
bounderID: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceFunc = vi.fn<
|
|
||||||
[ContainerEvent & { type: "SERVICE_BIND" }],
|
|
||||||
void
|
|
||||||
>()
|
|
||||||
|
|
||||||
container.getEventStream().subscribe((ev) => {
|
|
||||||
// We only care about the bind event of TestServiceA
|
|
||||||
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
|
|
||||||
serviceFunc(ev)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
container.bind(TestServiceB)
|
|
||||||
|
|
||||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
|
||||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
|
||||||
type: "SERVICE_BIND",
|
|
||||||
boundeeID: TestServiceA.ID,
|
|
||||||
bounderID: TestServiceB.ID,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("hasBound", () => {
|
|
||||||
it("returns true if the given service is bound to the container", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
container.bind(TestServiceA)
|
|
||||||
|
|
||||||
expect(container.hasBound(TestServiceA)).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns false if the given service is not bound to the container", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
expect(container.hasBound(TestServiceA)).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns true when the service is bound because it is a dependency of another service", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
container.bind(TestServiceB)
|
|
||||||
|
|
||||||
expect(container.hasBound(TestServiceA)).toEqual(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getEventStream", () => {
|
|
||||||
it("returns an observable which emits events correctly when services are initialized", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceFunc = vi.fn<
|
|
||||||
[ContainerEvent & { type: "SERVICE_INIT" }],
|
|
||||||
void
|
|
||||||
>()
|
|
||||||
|
|
||||||
container.getEventStream().subscribe((ev) => {
|
|
||||||
if (ev.type === "SERVICE_INIT") {
|
|
||||||
serviceFunc(ev)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
container.bind(TestServiceB)
|
|
||||||
|
|
||||||
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
|
||||||
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
|
||||||
type: "SERVICE_INIT",
|
|
||||||
serviceID: TestServiceA.ID,
|
|
||||||
})
|
|
||||||
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
|
||||||
type: "SERVICE_INIT",
|
|
||||||
serviceID: TestServiceB.ID,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns an observable which emits events correctly when services are bound", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceFunc = vi.fn<
|
|
||||||
[ContainerEvent & { type: "SERVICE_BIND" }],
|
|
||||||
void
|
|
||||||
>()
|
|
||||||
|
|
||||||
container.getEventStream().subscribe((ev) => {
|
|
||||||
if (ev.type === "SERVICE_BIND") {
|
|
||||||
serviceFunc(ev)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
container.bind(TestServiceB)
|
|
||||||
|
|
||||||
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
|
||||||
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
|
||||||
type: "SERVICE_BIND",
|
|
||||||
boundeeID: TestServiceA.ID,
|
|
||||||
bounderID: TestServiceB.ID,
|
|
||||||
})
|
|
||||||
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
|
||||||
type: "SERVICE_BIND",
|
|
||||||
boundeeID: TestServiceB.ID,
|
|
||||||
bounderID: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getBoundServices", () => {
|
|
||||||
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const instanceB = container.bind(TestServiceB)
|
|
||||||
const instanceA = instanceB.serviceA
|
|
||||||
|
|
||||||
expect(Array.from(container.getBoundServices())).toEqual([
|
|
||||||
[TestServiceA.ID, instanceA],
|
|
||||||
[TestServiceB.ID, instanceB],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns an empty iterator if no services are bound", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
expect(Array.from(container.getBoundServices())).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest"
|
|
||||||
import { Service, Container } from "../lib/main"
|
|
||||||
|
|
||||||
class TestServiceA extends Service {
|
|
||||||
public static ID = "TestServiceA"
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestServiceB extends Service<"test"> {
|
|
||||||
public static ID = "TestServiceB"
|
|
||||||
|
|
||||||
// Marked public to allow for testing
|
|
||||||
public readonly serviceA = this.bind(TestServiceA)
|
|
||||||
|
|
||||||
public emitTestEvent() {
|
|
||||||
this.emit("test")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Service", () => {
|
|
||||||
describe("constructor", () => {
|
|
||||||
it("throws an error if the service is initialized without a container", () => {
|
|
||||||
expect(() => new TestServiceA()).toThrowError(
|
|
||||||
"Tried to initialize service with no container (ID: TestServiceA)"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("bind", () => {
|
|
||||||
it("correctly binds the dependency service using the container", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceA = container.bind(TestServiceA)
|
|
||||||
|
|
||||||
const serviceB = container.bind(TestServiceB)
|
|
||||||
expect(serviceB.serviceA).toBe(serviceA)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getContainer", () => {
|
|
||||||
it("returns the container the service is bound to", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceA = container.bind(TestServiceA)
|
|
||||||
|
|
||||||
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
|
|
||||||
expect(serviceA.getContainer()).toBe(container)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("getEventStream", () => {
|
|
||||||
it("returns the valid event stream of the service", () => {
|
|
||||||
const container = new Container()
|
|
||||||
|
|
||||||
const serviceB = container.bind(TestServiceB)
|
|
||||||
|
|
||||||
const serviceFunc = vi.fn()
|
|
||||||
|
|
||||||
serviceB.getEventStream().subscribe(serviceFunc)
|
|
||||||
|
|
||||||
serviceB.emitTestEvent()
|
|
||||||
|
|
||||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
|
||||||
expect(serviceFunc).toHaveBeenCalledWith("test")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest"
|
|
||||||
import { TestContainer } from "../lib/testing"
|
|
||||||
import { Service } from "../lib/service"
|
|
||||||
import { ContainerEvent } from "../lib/container"
|
|
||||||
|
|
||||||
class TestServiceA extends Service {
|
|
||||||
public static ID = "TestServiceA"
|
|
||||||
|
|
||||||
public test() {
|
|
||||||
return "real"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestServiceB extends Service {
|
|
||||||
public static ID = "TestServiceB"
|
|
||||||
|
|
||||||
// declared public to help with testing
|
|
||||||
public readonly serviceA = this.bind(TestServiceA)
|
|
||||||
|
|
||||||
public test() {
|
|
||||||
return this.serviceA.test()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("TestContainer", () => {
|
|
||||||
describe("bindMock", () => {
|
|
||||||
it("returns the fake service defined", () => {
|
|
||||||
const container = new TestContainer()
|
|
||||||
|
|
||||||
const fakeService = {
|
|
||||||
test: () => "fake",
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = container.bindMock(TestServiceA, fakeService)
|
|
||||||
|
|
||||||
expect(result).toBe(fakeService)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("new services bound to the container get the mock service", () => {
|
|
||||||
const container = new TestContainer()
|
|
||||||
|
|
||||||
const fakeServiceA = {
|
|
||||||
test: () => "fake",
|
|
||||||
}
|
|
||||||
|
|
||||||
container.bindMock(TestServiceA, fakeServiceA)
|
|
||||||
|
|
||||||
const serviceB = container.bind(TestServiceB)
|
|
||||||
|
|
||||||
expect(serviceB.serviceA).toBe(fakeServiceA)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("container emits SERVICE_BIND event", () => {
|
|
||||||
const container = new TestContainer()
|
|
||||||
|
|
||||||
const fakeServiceA = {
|
|
||||||
test: () => "fake",
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceFunc = vi.fn<[ContainerEvent, void]>()
|
|
||||||
|
|
||||||
container.getEventStream().subscribe((ev) => {
|
|
||||||
serviceFunc(ev)
|
|
||||||
})
|
|
||||||
|
|
||||||
container.bindMock(TestServiceA, fakeServiceA)
|
|
||||||
|
|
||||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
|
||||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
|
||||||
type: "SERVICE_BIND",
|
|
||||||
boundeeID: TestServiceA.ID,
|
|
||||||
bounderID: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("throws if service already bound", () => {
|
|
||||||
const container = new TestContainer()
|
|
||||||
|
|
||||||
const fakeServiceA = {
|
|
||||||
test: () => "fake",
|
|
||||||
}
|
|
||||||
|
|
||||||
container.bindMock(TestServiceA, fakeServiceA)
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
container.bindMock(TestServiceA, fakeServiceA)
|
|
||||||
}).toThrowError(
|
|
||||||
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
2
packages/dioc/testing.d.ts
vendored
2
packages/dioc/testing.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./dist/testing.d.ts"
|
|
||||||
export * from "./dist/testing.d.ts"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"strict": true,
|
|
||||||
"declaration": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
|
||||||
"include": ["lib"]
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: {
|
|
||||||
index: './lib/main.ts',
|
|
||||||
vue: './lib/vue.ts',
|
|
||||||
testing: './lib/testing.ts',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: ['vue'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { defineConfig } from "vitest/config"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
2
packages/dioc/vue.d.ts
vendored
2
packages/dioc/vue.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./dist/vue.d.ts"
|
|
||||||
export * from "./dist/vue.d.ts"
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.4.8",
|
"version": "2023.4.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@nestjs/passport": "^9.0.0",
|
"@nestjs/passport": "^9.0.0",
|
||||||
"@nestjs/platform-express": "^9.2.1",
|
"@nestjs/platform-express": "^9.2.1",
|
||||||
"@nestjs/throttler": "^4.0.0",
|
"@nestjs/throttler": "^4.0.0",
|
||||||
"@prisma/client": "^4.16.2",
|
"@prisma/client": "^4.7.1",
|
||||||
"apollo-server-express": "^3.11.1",
|
"apollo-server-express": "^3.11.1",
|
||||||
"apollo-server-plugin-base": "^3.7.1",
|
"apollo-server-plugin-base": "^3.7.1",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-microsoft": "^1.0.0",
|
||||||
"prisma": "^4.16.2",
|
"prisma": "^4.7.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.6.0"
|
"rxjs": "^7.6.0"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ datasource db {
|
|||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"]
|
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Team {
|
model Team {
|
||||||
|
|||||||
@@ -411,23 +411,6 @@ export class AdminResolver {
|
|||||||
return deletedTeam.right;
|
return deletedTeam.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
|
||||||
description: 'Revoke a team Invite by Invite ID',
|
|
||||||
})
|
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
|
||||||
async revokeTeamInviteByAdmin(
|
|
||||||
@Args({
|
|
||||||
name: 'inviteID',
|
|
||||||
description: 'Team Invite ID',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
inviteID: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const invite = await this.adminService.revokeTeamInviteByID(inviteID);
|
|
||||||
if (E.isLeft(invite)) throwErr(invite.left);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|
||||||
@Subscription(() => InvitedUser, {
|
@Subscription(() => InvitedUser, {
|
||||||
|
|||||||
@@ -10,11 +10,23 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
|
|||||||
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
||||||
import { MailerService } from '../mailer/mailer.service';
|
import { MailerService } from '../mailer/mailer.service';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { User as DbUser } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
DUPLICATE_EMAIL,
|
DUPLICATE_EMAIL,
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
|
ONLY_ONE_ADMIN_ACCOUNT,
|
||||||
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
|
USER_IS_ADMIN,
|
||||||
|
USER_NOT_FOUND,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
|
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||||
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import * as O from 'fp-ts/Option';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
import * as utils from 'src/utils';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -52,7 +64,582 @@ const invitedUsers: InvitedUsers[] = [
|
|||||||
invitedOn: new Date(),
|
invitedOn: new Date(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const allUsers: DbUser[] = [
|
||||||
|
{
|
||||||
|
uid: 'uid1',
|
||||||
|
displayName: 'user1',
|
||||||
|
email: 'user1@hoppscotch.io',
|
||||||
|
photoURL: 'https://hoppscotch.io',
|
||||||
|
isAdmin: true,
|
||||||
|
refreshToken: 'refreshToken',
|
||||||
|
currentRESTSession: null,
|
||||||
|
currentGQLSession: null,
|
||||||
|
createdOn: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uid: 'uid2',
|
||||||
|
displayName: 'user2',
|
||||||
|
email: 'user2@hoppscotch.io',
|
||||||
|
photoURL: 'https://hoppscotch.io',
|
||||||
|
isAdmin: false,
|
||||||
|
refreshToken: 'refreshToken',
|
||||||
|
currentRESTSession: null,
|
||||||
|
currentGQLSession: null,
|
||||||
|
createdOn: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const teamMembers: TeamMember[] = [
|
||||||
|
{
|
||||||
|
membershipID: 'teamMember1',
|
||||||
|
userUid: allUsers[0].uid,
|
||||||
|
role: TeamMemberRole.OWNER,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const teams: Team[] = [
|
||||||
|
{
|
||||||
|
id: 'team1',
|
||||||
|
name: 'team1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'team2',
|
||||||
|
name: 'team2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const teamInvitations: TeamInvitation[] = [
|
||||||
|
{
|
||||||
|
id: 'teamInvitation1',
|
||||||
|
teamID: 'team1',
|
||||||
|
creatorUid: 'uid1',
|
||||||
|
inviteeEmail: '',
|
||||||
|
inviteeRole: TeamMemberRole.OWNER,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('AdminService', () => {
|
describe('AdminService', () => {
|
||||||
|
describe('fetchUsers', () => {
|
||||||
|
test('should resolve right and return an array of users if cursorID is null', async () => {
|
||||||
|
mockUserService.fetchAllUsers.mockResolvedValueOnce(allUsers);
|
||||||
|
|
||||||
|
const result = await adminService.fetchUsers(null, 10);
|
||||||
|
|
||||||
|
expect(result).toEqual(allUsers);
|
||||||
|
expect(mockUserService.fetchAllUsers).toHaveBeenCalledWith(null, 10);
|
||||||
|
});
|
||||||
|
test('should resolve right and return an array of users if cursorID is not null', async () => {
|
||||||
|
mockUserService.fetchAllUsers.mockResolvedValueOnce([allUsers[1]]);
|
||||||
|
|
||||||
|
const cursorID = allUsers[0].uid;
|
||||||
|
const result = await adminService.fetchUsers(cursorID, 10);
|
||||||
|
|
||||||
|
expect(result).toEqual([allUsers[1]]);
|
||||||
|
expect(mockUserService.fetchAllUsers).toHaveBeenCalledWith(cursorID, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAllTeams', () => {
|
||||||
|
test('should resolve right and return an array of teams if cursorID is null', async () => {
|
||||||
|
mockTeamService.fetchAllTeams.mockResolvedValueOnce(teams);
|
||||||
|
|
||||||
|
const result = await adminService.fetchAllTeams(null, 10);
|
||||||
|
|
||||||
|
expect(result).toEqual(teams);
|
||||||
|
expect(mockTeamService.fetchAllTeams).toHaveBeenCalledWith(null, 10);
|
||||||
|
});
|
||||||
|
test('should resolve right and return an array of teams if cursorID is not null', async () => {
|
||||||
|
mockTeamService.fetchAllTeams.mockResolvedValueOnce([teams[1]]);
|
||||||
|
|
||||||
|
const cursorID = teams[0].id;
|
||||||
|
const result = await adminService.fetchAllTeams(cursorID, 10);
|
||||||
|
|
||||||
|
expect(result).toEqual([teams[1]]);
|
||||||
|
expect(mockTeamService.fetchAllTeams).toHaveBeenCalledWith(cursorID, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('membersCountInTeam', () => {
|
||||||
|
test('should resolve right and return the count of members in a team', async () => {
|
||||||
|
mockTeamService.getCountOfMembersInTeam.mockResolvedValueOnce(10);
|
||||||
|
|
||||||
|
const result = await adminService.membersCountInTeam('team1');
|
||||||
|
|
||||||
|
expect(result).toEqual(10);
|
||||||
|
expect(mockTeamService.getCountOfMembersInTeam).toHaveBeenCalledWith(
|
||||||
|
'team1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('collectionCountInTeam', () => {
|
||||||
|
test('should resolve right and return the count of collections in a team', async () => {
|
||||||
|
mockTeamCollectionService.totalCollectionsInTeam.mockResolvedValueOnce(
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await adminService.collectionCountInTeam('team1');
|
||||||
|
|
||||||
|
expect(result).toEqual(10);
|
||||||
|
expect(
|
||||||
|
mockTeamCollectionService.totalCollectionsInTeam,
|
||||||
|
).toHaveBeenCalledWith('team1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requestCountInTeam', () => {
|
||||||
|
test('should resolve right and return the count of requests in a team', async () => {
|
||||||
|
mockTeamRequestService.totalRequestsInATeam.mockResolvedValueOnce(10);
|
||||||
|
|
||||||
|
const result = await adminService.requestCountInTeam('team1');
|
||||||
|
|
||||||
|
expect(result).toEqual(10);
|
||||||
|
expect(mockTeamRequestService.totalRequestsInATeam).toHaveBeenCalledWith(
|
||||||
|
'team1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('environmentCountInTeam', () => {
|
||||||
|
test('should resolve right and return the count of environments in a team', async () => {
|
||||||
|
mockTeamEnvironmentsService.totalEnvsInTeam.mockResolvedValueOnce(10);
|
||||||
|
|
||||||
|
const result = await adminService.environmentCountInTeam('team1');
|
||||||
|
|
||||||
|
expect(result).toEqual(10);
|
||||||
|
expect(mockTeamEnvironmentsService.totalEnvsInTeam).toHaveBeenCalledWith(
|
||||||
|
'team1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pendingInvitationCountInTeam', () => {
|
||||||
|
test('should resolve right and return the count of pending invitations in a team', async () => {
|
||||||
|
mockTeamInvitationService.getTeamInvitations.mockResolvedValueOnce(
|
||||||
|
teamInvitations,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await adminService.pendingInvitationCountInTeam('team1');
|
||||||
|
|
||||||
|
expect(result).toEqual(teamInvitations);
|
||||||
|
expect(
|
||||||
|
mockTeamInvitationService.getTeamInvitations,
|
||||||
|
).toHaveBeenCalledWith('team1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changeRoleOfUserTeam', () => {
|
||||||
|
test('should resolve right and return the count of pending invitations in a team', async () => {
|
||||||
|
const teamMember = teamMembers[0];
|
||||||
|
|
||||||
|
mockTeamService.updateTeamMemberRole.mockResolvedValueOnce(
|
||||||
|
E.right(teamMember),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await adminService.changeRoleOfUserTeam(
|
||||||
|
teamMember.userUid,
|
||||||
|
'team1',
|
||||||
|
teamMember.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualRight(teamMember);
|
||||||
|
expect(mockTeamService.updateTeamMemberRole).toHaveBeenCalledWith(
|
||||||
|
'team1',
|
||||||
|
teamMember.userUid,
|
||||||
|
teamMember.role,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve left and return the error if any error occurred', async () => {
|
||||||
|
const teamMember = teamMembers[0];
|
||||||
|
const errorMessage = 'Team member not found';
|
||||||
|
|
||||||
|
mockTeamService.updateTeamMemberRole.mockResolvedValueOnce(
|
||||||
|
E.left(errorMessage),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await adminService.changeRoleOfUserTeam(
|
||||||
|
teamMember.userUid,
|
||||||
|
'team1',
|
||||||
|
teamMember.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(errorMessage);
|
||||||
|
expect(mockTeamService.updateTeamMemberRole).toHaveBeenCalledWith(
|
||||||
|
'team1',
|
||||||
|
teamMember.userUid,
|
||||||
|
teamMember.role,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeUserFromTeam', () => {
|
||||||
|
test('should resolve right and remove user from a team', async () => {
|
||||||
|
const teamMember = teamMembers[0];
|
||||||
|
|
||||||
|
mockTeamService.leaveTeam.mockResolvedValueOnce(E.right(true));
|
||||||
|
|
||||||
|
const result = await adminService.removeUserFromTeam(
|
||||||
|
teamMember.userUid,
|
||||||
|
'team1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualRight(true);
|
||||||
|
expect(mockTeamService.leaveTeam).toHaveBeenCalledWith(
|
||||||
|
'team1',
|
||||||
|
teamMember.userUid,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve left and return the error if any error occurred', async () => {
|
||||||
|
const teamMember = teamMembers[0];
|
||||||
|
const errorMessage = 'Team member not found';
|
||||||
|
|
||||||
|
mockTeamService.leaveTeam.mockResolvedValueOnce(E.left(errorMessage));
|
||||||
|
|
||||||
|
const result = await adminService.removeUserFromTeam(
|
||||||
|
teamMember.userUid,
|
||||||
|
'team1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(errorMessage);
|
||||||
|
expect(mockTeamService.leaveTeam).toHaveBeenCalledWith(
|
||||||
|
'team1',
|
||||||
|
teamMember.userUid,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addUserToTeam', () => {
|
||||||
|
test('should return INVALID_EMAIL when email is invalid', async () => {
|
||||||
|
const teamID = 'team1';
|
||||||
|
const userEmail = 'invalidEmail';
|
||||||
|
const role = TeamMemberRole.EDITOR;
|
||||||
|
|
||||||
|
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
||||||
|
mockValidateEmail.mockReturnValueOnce(false);
|
||||||
|
|
||||||
|
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(INVALID_EMAIL));
|
||||||
|
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
||||||
|
expect(mockUserService.findUserByEmail).not.toHaveBeenCalled();
|
||||||
|
expect(mockTeamService.getTeamMemberTE).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return USER_NOT_FOUND when user is not found', async () => {
|
||||||
|
const teamID = 'team1';
|
||||||
|
const userEmail = 'u@example.com';
|
||||||
|
const role = TeamMemberRole.EDITOR;
|
||||||
|
|
||||||
|
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
||||||
|
mockValidateEmail.mockReturnValueOnce(true);
|
||||||
|
mockUserService.findUserByEmail.mockResolvedValue(O.none);
|
||||||
|
|
||||||
|
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
||||||
|
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return TEAM_INVITE_ALREADY_MEMBER when user is already a member of the team', async () => {
|
||||||
|
const teamID = 'team1';
|
||||||
|
const userEmail = allUsers[0].email;
|
||||||
|
const role = TeamMemberRole.EDITOR;
|
||||||
|
|
||||||
|
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
||||||
|
mockValidateEmail.mockReturnValueOnce(true);
|
||||||
|
mockUserService.findUserByEmail.mockResolvedValueOnce(
|
||||||
|
O.some(allUsers[0]),
|
||||||
|
);
|
||||||
|
mockTeamService.getTeamMemberTE.mockReturnValueOnce(
|
||||||
|
TE.right(teamMembers[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(TEAM_INVITE_ALREADY_MEMBER));
|
||||||
|
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
||||||
|
expect(mockUserService.findUserByEmail).toHaveBeenCalledWith(userEmail);
|
||||||
|
expect(mockTeamService.getTeamMemberTE).toHaveBeenCalledWith(
|
||||||
|
teamID,
|
||||||
|
allUsers[0].uid,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add user to the team and return the result when user is not a member of the team', async () => {
|
||||||
|
const teamID = 'team1';
|
||||||
|
const userEmail = allUsers[0].email;
|
||||||
|
const role = TeamMemberRole.EDITOR;
|
||||||
|
|
||||||
|
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
||||||
|
mockValidateEmail.mockReturnValueOnce(true);
|
||||||
|
mockUserService.findUserByEmail.mockResolvedValueOnce(
|
||||||
|
O.some(allUsers[0]),
|
||||||
|
);
|
||||||
|
mockTeamService.getTeamMemberTE.mockReturnValueOnce(
|
||||||
|
TE.left(TEAM_MEMBER_NOT_FOUND),
|
||||||
|
);
|
||||||
|
mockTeamService.addMemberToTeamWithEmail.mockResolvedValueOnce(
|
||||||
|
E.right(teamMembers[0]),
|
||||||
|
);
|
||||||
|
mockTeamInvitationService.getTeamInviteByEmailAndTeamID.mockResolvedValueOnce(
|
||||||
|
E.right(teamInvitations[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(teamMembers[0]));
|
||||||
|
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
||||||
|
expect(mockUserService.findUserByEmail).toHaveBeenCalledWith(userEmail);
|
||||||
|
expect(mockTeamService.getTeamMemberTE).toHaveBeenCalledWith(
|
||||||
|
teamID,
|
||||||
|
allUsers[0].uid,
|
||||||
|
);
|
||||||
|
expect(mockTeamService.addMemberToTeamWithEmail).toHaveBeenCalledWith(
|
||||||
|
teamID,
|
||||||
|
allUsers[0].email,
|
||||||
|
role,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createATeam', () => {
|
||||||
|
test('should return USER_NOT_FOUND when user is not found', async () => {
|
||||||
|
const userUid = allUsers[0].uid;
|
||||||
|
const teamName = 'team1';
|
||||||
|
|
||||||
|
mockUserService.findUserById.mockResolvedValue(O.none);
|
||||||
|
|
||||||
|
const result = await adminService.createATeam(userUid, teamName);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
||||||
|
expect(mockUserService.findUserById).toHaveBeenCalledWith(userUid);
|
||||||
|
expect(mockTeamService.createTeam).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create a team and return the result when the team is created successfully', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
const team = teams[0];
|
||||||
|
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
||||||
|
mockTeamService.createTeam.mockResolvedValueOnce(E.right(team));
|
||||||
|
|
||||||
|
const result = await adminService.createATeam(user.uid, team.name);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(team));
|
||||||
|
expect(mockUserService.findUserById).toHaveBeenCalledWith(user.uid);
|
||||||
|
expect(mockTeamService.createTeam).toHaveBeenCalledWith(
|
||||||
|
team.name,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the error when the team creation fails', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
const team = teams[0];
|
||||||
|
const errorMessage = 'error';
|
||||||
|
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
||||||
|
mockTeamService.createTeam.mockResolvedValueOnce(E.left(errorMessage));
|
||||||
|
|
||||||
|
const result = await adminService.createATeam(user.uid, team.name);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(errorMessage));
|
||||||
|
expect(mockUserService.findUserById).toHaveBeenCalledWith(user.uid);
|
||||||
|
expect(mockTeamService.createTeam).toHaveBeenCalledWith(
|
||||||
|
team.name,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renameATeam', () => {
|
||||||
|
test('should rename a team and return the result when the team is renamed successfully', async () => {
|
||||||
|
const team = teams[0];
|
||||||
|
const newName = 'new name';
|
||||||
|
|
||||||
|
mockTeamService.renameTeam.mockResolvedValueOnce(E.right(team));
|
||||||
|
|
||||||
|
const result = await adminService.renameATeam(team.id, newName);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(team));
|
||||||
|
expect(mockTeamService.renameTeam).toHaveBeenCalledWith(team.id, newName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the error when the team renaming fails', async () => {
|
||||||
|
const team = teams[0];
|
||||||
|
const newName = 'new name';
|
||||||
|
const errorMessage = 'error';
|
||||||
|
|
||||||
|
mockTeamService.renameTeam.mockResolvedValueOnce(E.left(errorMessage));
|
||||||
|
|
||||||
|
const result = await adminService.renameATeam(team.id, newName);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(errorMessage));
|
||||||
|
expect(mockTeamService.renameTeam).toHaveBeenCalledWith(team.id, newName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteATeam', () => {
|
||||||
|
test('should delete a team and return the result when the team is deleted successfully', async () => {
|
||||||
|
const team = teams[0];
|
||||||
|
|
||||||
|
mockTeamService.deleteTeam.mockResolvedValueOnce(E.right(true));
|
||||||
|
|
||||||
|
const result = await adminService.deleteATeam(team.id);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(true));
|
||||||
|
expect(mockTeamService.deleteTeam).toHaveBeenCalledWith(team.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the error when the team deletion fails', async () => {
|
||||||
|
const team = teams[0];
|
||||||
|
const errorMessage = 'error';
|
||||||
|
|
||||||
|
mockTeamService.deleteTeam.mockResolvedValueOnce(E.left(errorMessage));
|
||||||
|
|
||||||
|
const result = await adminService.deleteATeam(team.id);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(errorMessage));
|
||||||
|
expect(mockTeamService.deleteTeam).toHaveBeenCalledWith(team.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAdmins', () => {
|
||||||
|
test('should return the list of admin users', async () => {
|
||||||
|
const adminUsers = [];
|
||||||
|
mockUserService.fetchAdminUsers.mockResolvedValueOnce(adminUsers);
|
||||||
|
const result = await adminService.fetchAdmins();
|
||||||
|
|
||||||
|
expect(result).toEqual(adminUsers);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchUserInfo', () => {
|
||||||
|
test('should return the user info when the user is found', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
||||||
|
const result = await adminService.fetchUserInfo(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(user));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return USER_NOT_FOUND when the user is not found', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.none);
|
||||||
|
const result = await adminService.fetchUserInfo(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeUserAccount', () => {
|
||||||
|
test('should return USER_NOT_FOUND when the user is not found', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.none);
|
||||||
|
const result = await adminService.removeUserAccount(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return USER_IS_ADMIN when the user is an admin', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
||||||
|
const result = await adminService.removeUserAccount(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_IS_ADMIN));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove the user account and return the result when the user is not an admin', async () => {
|
||||||
|
const user = allUsers[1];
|
||||||
|
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
||||||
|
mockUserService.deleteUserByUID.mockReturnValueOnce(TE.right(true));
|
||||||
|
const result = await adminService.removeUserAccount(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the error when the user account deletion fails', async () => {
|
||||||
|
const user = allUsers[1];
|
||||||
|
const errorMessage = 'error';
|
||||||
|
|
||||||
|
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
||||||
|
mockUserService.deleteUserByUID.mockReturnValueOnce(
|
||||||
|
TE.left(errorMessage),
|
||||||
|
);
|
||||||
|
const result = await adminService.removeUserAccount(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(errorMessage));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeUserAdmin', () => {
|
||||||
|
test('should make the user an admin and return true when the operation is successful', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
|
||||||
|
mockUserService.makeAdmin.mockResolvedValueOnce(E.right(user));
|
||||||
|
const result = await adminService.makeUserAdmin(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the error when making the user an admin fails', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
|
||||||
|
mockUserService.makeAdmin.mockResolvedValueOnce(E.left(USER_NOT_FOUND));
|
||||||
|
const result = await adminService.makeUserAdmin(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeUserAsAdmin', () => {
|
||||||
|
test('should return ONLY_ONE_ADMIN_ACCOUNT when there is only one admin account', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
|
||||||
|
mockUserService.fetchAdminUsers.mockResolvedValueOnce([user]);
|
||||||
|
const result = await adminService.removeUserAsAdmin(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(ONLY_ONE_ADMIN_ACCOUNT));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove the user as an admin and return true when the operation is successful', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
|
||||||
|
mockUserService.fetchAdminUsers.mockResolvedValueOnce(allUsers);
|
||||||
|
mockUserService.removeUserAsAdmin.mockResolvedValueOnce(E.right(user));
|
||||||
|
const result = await adminService.removeUserAsAdmin(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the error when removing the user as an admin fails', async () => {
|
||||||
|
const user = allUsers[0];
|
||||||
|
|
||||||
|
mockUserService.fetchAdminUsers.mockResolvedValueOnce(allUsers);
|
||||||
|
mockUserService.removeUserAsAdmin.mockResolvedValueOnce(
|
||||||
|
E.left(USER_NOT_FOUND),
|
||||||
|
);
|
||||||
|
const result = await adminService.removeUserAsAdmin(user.uid);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTeamInfo', () => {
|
||||||
|
test('should return the team info when the team is found', async () => {
|
||||||
|
const team = teams[0];
|
||||||
|
mockTeamService.getTeamWithIDTE.mockReturnValue(TE.right(team));
|
||||||
|
const result = await adminService.getTeamInfo(team.id);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.right(team));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('fetchInvitedUsers', () => {
|
describe('fetchInvitedUsers', () => {
|
||||||
test('should resolve right and return an array of invited users', async () => {
|
test('should resolve right and return an array of invited users', async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
ONLY_ONE_ADMIN_ACCOUNT,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_IS_ADMIN,
|
USER_IS_ADMIN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
@@ -241,6 +240,7 @@ export class AdminService {
|
|||||||
teamID,
|
teamID,
|
||||||
user.value.uid,
|
user.value.uid,
|
||||||
)();
|
)();
|
||||||
|
|
||||||
if (E.isLeft(teamMember)) {
|
if (E.isLeft(teamMember)) {
|
||||||
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
||||||
teamID,
|
teamID,
|
||||||
@@ -417,19 +417,4 @@ export class AdminService {
|
|||||||
if (E.isLeft(team)) return E.left(team.left);
|
if (E.isLeft(team)) return E.left(team.left);
|
||||||
return E.right(team.right);
|
return E.right(team.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Revoke a team invite by ID
|
|
||||||
* @param inviteID Team Invite ID
|
|
||||||
* @returns an Either of boolean or error
|
|
||||||
*/
|
|
||||||
async revokeTeamInviteByID(inviteID: string) {
|
|
||||||
const teamInvite = await this.teamInvitationService.revokeInvitation(
|
|
||||||
inviteID,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(teamInvite)) return E.left(teamInvite.left);
|
|
||||||
|
|
||||||
return E.right(teamInvite.right);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Controller('ping')
|
|
||||||
export class AppController {
|
|
||||||
@Get()
|
|
||||||
ping(): string {
|
|
||||||
return 'Success';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
|
|||||||
import { ShortcodeModule } from './shortcode/shortcode.module';
|
import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||||
import { COOKIES_NOT_FOUND } from './errors';
|
import { COOKIES_NOT_FOUND } from './errors';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
import { AppController } from './app.controller';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -82,6 +81,5 @@ import { AppController } from './app.controller';
|
|||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
controllers: [AppController],
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
InternalServerErrorException,
|
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
|
Req,
|
||||||
Request,
|
Request,
|
||||||
Res,
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -19,18 +19,12 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import {
|
import { authCookieHandler, throwHTTPErr } from './helper';
|
||||||
AuthProvider,
|
|
||||||
authCookieHandler,
|
|
||||||
authProviderCheck,
|
|
||||||
throwHTTPErr,
|
|
||||||
} from './helper';
|
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
@@ -45,9 +39,6 @@ export class AuthController {
|
|||||||
@Body() authData: SignInMagicDto,
|
@Body() authData: SignInMagicDto,
|
||||||
@Query('origin') origin: string,
|
@Query('origin') origin: string,
|
||||||
) {
|
) {
|
||||||
if (!authProviderCheck(AuthProvider.EMAIL))
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
|
||||||
|
|
||||||
const deviceIdToken = await this.authService.signInMagicLink(
|
const deviceIdToken = await this.authService.signInMagicLink(
|
||||||
authData.email,
|
authData.email,
|
||||||
origin,
|
origin,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
|
|||||||
import { GoogleStrategy } from './strategies/google.strategy';
|
import { GoogleStrategy } from './strategies/google.strategy';
|
||||||
import { GithubStrategy } from './strategies/github.strategy';
|
import { GithubStrategy } from './strategies/github.strategy';
|
||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -27,9 +26,9 @@ import { AuthProvider, authProviderCheck } from './helper';
|
|||||||
AuthService,
|
AuthService,
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
RTJwtStrategy,
|
RTJwtStrategy,
|
||||||
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
GoogleStrategy,
|
||||||
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
GithubStrategy,
|
||||||
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
MicrosoftStrategy,
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
export class GithubSSOGuard extends AuthGuard('github') {
|
||||||
canActivate(
|
|
||||||
context: ExecutionContext,
|
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
|
||||||
if (!authProviderCheck(AuthProvider.GITHUB))
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
export class GoogleSSOGuard extends AuthGuard('google') {
|
||||||
canActivate(
|
|
||||||
context: ExecutionContext,
|
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
|
||||||
if (!authProviderCheck(AuthProvider.GOOGLE))
|
|
||||||
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,8 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard
|
export class MicrosoftSSOGuard extends AuthGuard('microsoft') {
|
||||||
extends AuthGuard('microsoft')
|
|
||||||
implements CanActivate
|
|
||||||
{
|
|
||||||
canActivate(
|
|
||||||
context: ExecutionContext,
|
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
|
||||||
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
|
||||||
throwHTTPErr({
|
|
||||||
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
|
||||||
statusCode: 404,
|
|
||||||
});
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
import { ForbiddenException, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { AuthError } from 'src/types/AuthError';
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
import { COOKIES_NOT_FOUND } from 'src/errors';
|
||||||
import { throwErr } from 'src/utils';
|
|
||||||
|
|
||||||
enum AuthTokenType {
|
enum AuthTokenType {
|
||||||
ACCESS_TOKEN = 'access_token',
|
ACCESS_TOKEN = 'access_token',
|
||||||
@@ -17,13 +16,6 @@ export enum Origin {
|
|||||||
APP = 'app',
|
APP = 'app',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthProvider {
|
|
||||||
GOOGLE = 'GOOGLE',
|
|
||||||
GITHUB = 'GITHUB',
|
|
||||||
MICROSOFT = 'MICROSOFT',
|
|
||||||
EMAIL = 'EMAIL',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function allows throw to be used as an expression
|
* This function allows throw to be used as an expression
|
||||||
* @param errMessage Message present in the error message
|
* @param errMessage Message present in the error message
|
||||||
@@ -105,25 +97,3 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
|||||||
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
|
|
||||||
*
|
|
||||||
* @param provider Provider we want to check the presence of
|
|
||||||
* @returns Boolean if provider specified is present or not
|
|
||||||
*/
|
|
||||||
export function authProviderCheck(provider: string) {
|
|
||||||
if (!provider) {
|
|
||||||
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
|
||||||
}
|
|
||||||
|
|
||||||
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
|
||||||
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
|
||||||
provider.trim().toUpperCase(),
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!envVariables.includes(provider.toUpperCase())) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,30 +22,6 @@ export const AUTH_FAIL = 'auth/fail';
|
|||||||
*/
|
*/
|
||||||
export const JSON_INVALID = 'json_invalid';
|
export const JSON_INVALID = 'json_invalid';
|
||||||
|
|
||||||
/**
|
|
||||||
* Auth Provider not specified
|
|
||||||
* (Auth)
|
|
||||||
*/
|
|
||||||
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
|
||||||
*/
|
|
||||||
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
|
|
||||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
|
||||||
*/
|
|
||||||
export const ENV_EMPTY_AUTH_PROVIDERS =
|
|
||||||
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
|
||||||
*/
|
|
||||||
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
|
|
||||||
'"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to delete a user data document from fb firestore but failed.
|
* Tried to delete a user data document from fb firestore but failed.
|
||||||
* (FirebaseService)
|
* (FirebaseService)
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ import * as cookieParser from 'cookie-parser';
|
|||||||
import { VersioningType } from '@nestjs/common';
|
import { VersioningType } from '@nestjs/common';
|
||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
import { checkEnvironmentAuthProvider } from './utils';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||||
console.log(`Port: ${process.env.PORT}`);
|
console.log(`Port: ${process.env.PORT}`);
|
||||||
|
|
||||||
checkEnvironmentAuthProvider();
|
|
||||||
|
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdOn: 'desc',
|
createdOn: 'desc',
|
||||||
},
|
},
|
||||||
skip: args.cursor ? 1 : 0,
|
skip: 1,
|
||||||
take: args.take,
|
take: args.take,
|
||||||
cursor: args.cursor ? { id: args.cursor } : undefined,
|
cursor: args.cursor ? { id: args.cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import {
|
||||||
import { mock, mockDeep, mockReset } from 'jest-mock-extended';
|
Team,
|
||||||
|
TeamCollection as DBTeamCollection,
|
||||||
|
TeamRequest as DBTeamRequest,
|
||||||
|
} from '@prisma/client';
|
||||||
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
TEAM_COLL_DEST_SAME,
|
TEAM_COLL_DEST_SAME,
|
||||||
TEAM_COLL_INVALID_JSON,
|
TEAM_COLL_INVALID_JSON,
|
||||||
@@ -17,9 +21,8 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { TeamCollectionService } from './team-collection.service';
|
import { TeamCollectionService } from './team-collection.service';
|
||||||
import { TeamCollection } from './team-collection.model';
|
|
||||||
import { TeamCollectionModule } from './team-collection.module';
|
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -276,11 +279,188 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const teamRequestList: DBTeamRequest[] = [
|
||||||
|
{
|
||||||
|
id: 'req1',
|
||||||
|
collectionID: childTeamCollection.id,
|
||||||
|
teamID: team.id,
|
||||||
|
title: 'request 1',
|
||||||
|
request: {},
|
||||||
|
orderIndex: 1,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('exportCollectionsToJSON', () => {
|
||||||
|
test('should export collections to JSON string successfully for structure-1', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> <no request of child coll>
|
||||||
|
|-> <no request of root coll>
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
||||||
|
rootTeamCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Root Collection
|
||||||
|
jest
|
||||||
|
.spyOn(teamCollectionService, 'getCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootTeamCollection));
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childTeamCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 2: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Child Collection
|
||||||
|
jest
|
||||||
|
.spyOn(teamCollectionService, 'getCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(childTeamCollection));
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
mockPrisma.teamRequest.findMany.mockResolvedValueOnce([]);
|
||||||
|
// return { name: childTeamCollection.title, folders: [], requests: [], };
|
||||||
|
|
||||||
|
// Back to RCV CALL 1
|
||||||
|
mockPrisma.teamRequest.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const returnedValue: CollectionFolder = {
|
||||||
|
name: rootTeamCollection.title,
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
name: childTeamCollection.title,
|
||||||
|
folders: [],
|
||||||
|
requests: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requests: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await teamCollectionService.exportCollectionsToJSON(team.id);
|
||||||
|
expect(result).toEqualRight(JSON.stringify([returnedValue]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should export collections to JSON string successfully for structure-2', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> request1
|
||||||
|
|-> <no request of root coll>
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
||||||
|
rootTeamCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Root Collection
|
||||||
|
jest
|
||||||
|
.spyOn(teamCollectionService, 'getCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootTeamCollection));
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childTeamCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 2: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Child Collection
|
||||||
|
jest
|
||||||
|
.spyOn(teamCollectionService, 'getCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(childTeamCollection));
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
mockPrisma.teamRequest.findMany.mockResolvedValueOnce(teamRequestList);
|
||||||
|
// return { name: childTeamCollection.title, folders: [], requests: teamRequestList, };
|
||||||
|
|
||||||
|
// Back to RCV CALL 1
|
||||||
|
mockPrisma.teamRequest.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const returnedValue: CollectionFolder = {
|
||||||
|
name: rootTeamCollection.title,
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
name: childTeamCollection.title,
|
||||||
|
folders: [],
|
||||||
|
requests: teamRequestList.map((req) => req.request),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requests: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await teamCollectionService.exportCollectionsToJSON(team.id);
|
||||||
|
expect(result).toEqualRight(JSON.stringify([returnedValue]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should export collections to JSON string successfully for structure-3', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> child-request1
|
||||||
|
|-> root-request1
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
||||||
|
rootTeamCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Root Collection
|
||||||
|
jest
|
||||||
|
.spyOn(teamCollectionService, 'getCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootTeamCollection));
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childTeamCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 2: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Child Collection
|
||||||
|
jest
|
||||||
|
.spyOn(teamCollectionService, 'getCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(childTeamCollection));
|
||||||
|
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
mockPrisma.teamRequest.findMany.mockResolvedValueOnce(teamRequestList);
|
||||||
|
// return { name: childTeamCollection.title, folders: [], requests: teamRequestList, };
|
||||||
|
|
||||||
|
// Back to RCV CALL 1
|
||||||
|
mockPrisma.teamRequest.findMany.mockResolvedValueOnce(teamRequestList);
|
||||||
|
|
||||||
|
const returnedValue: CollectionFolder = {
|
||||||
|
name: rootTeamCollection.title,
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
name: childTeamCollection.title,
|
||||||
|
folders: [],
|
||||||
|
requests: teamRequestList.map((req) => req.request),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requests: teamRequestList.map((req) => req.request),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await teamCollectionService.exportCollectionsToJSON(team.id);
|
||||||
|
expect(result).toEqualRight(JSON.stringify([returnedValue]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCollectionCount', () => {
|
||||||
|
test('should return the count of collections successfully', async () => {
|
||||||
|
const count = 10;
|
||||||
|
|
||||||
|
mockPrisma.teamCollection.count.mockResolvedValueOnce(count);
|
||||||
|
const result = await teamCollectionService.getCollectionCount(
|
||||||
|
rootTeamCollection.id,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(count);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getTeamOfCollection', () => {
|
describe('getTeamOfCollection', () => {
|
||||||
test('should return the team of a collection successfully with valid collectionID', async () => {
|
test('should return the team of a collection successfully with valid collectionID', async () => {
|
||||||
mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({
|
mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({
|
||||||
@@ -1460,5 +1640,3 @@ describe('totalCollectionsInTeam', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//ToDo: write test cases for exportCollectionsToJSON
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { TeamService } from 'src/team/team.service';
|
|||||||
* This guard only allows user to execute the resolver
|
* This guard only allows user to execute the resolver
|
||||||
* 1. If user is invitee, allow
|
* 1. If user is invitee, allow
|
||||||
* 2. Or else, if user is team member, allow
|
* 2. Or else, if user is team member, allow
|
||||||
*
|
*
|
||||||
* TLDR: Allow if user is invitee or team member
|
* TLDR: Allow if user is invitee or team member
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
TEAM_REQ_NOT_FOUND,
|
TEAM_REQ_NOT_FOUND,
|
||||||
TEAM_REQ_REORDERING_FAILED,
|
TEAM_REQ_REORDERING_FAILED,
|
||||||
TEAM_COLL_NOT_FOUND,
|
TEAM_COLL_NOT_FOUND,
|
||||||
|
JSON_INVALID,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
@@ -239,7 +240,7 @@ describe('deleteTeamRequest', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createTeamRequest', () => {
|
describe('createTeamRequest', () => {
|
||||||
test('rejects for invalid collection id', async () => {
|
test('should rejects for invalid collection id', async () => {
|
||||||
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
||||||
E.left(TEAM_INVALID_COLL_ID),
|
E.left(TEAM_INVALID_COLL_ID),
|
||||||
);
|
);
|
||||||
@@ -255,7 +256,42 @@ describe('createTeamRequest', () => {
|
|||||||
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolves for valid collection id', async () => {
|
test('should rejects for invalid team ID', async () => {
|
||||||
|
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
||||||
|
E.right(team),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await teamRequestService.createTeamRequest(
|
||||||
|
'testcoll',
|
||||||
|
'invalidteamid',
|
||||||
|
'Test Request',
|
||||||
|
'{}',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqualLeft(TEAM_INVALID_ID);
|
||||||
|
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject for invalid request body', async () => {
|
||||||
|
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
||||||
|
E.right(team),
|
||||||
|
);
|
||||||
|
teamRequestService.getRequestsCountInCollection = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(0);
|
||||||
|
|
||||||
|
const response = await teamRequestService.createTeamRequest(
|
||||||
|
'testcoll',
|
||||||
|
team.id,
|
||||||
|
'Test Request',
|
||||||
|
'invalidjson',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqualLeft(JSON_INVALID);
|
||||||
|
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolves and create team request', async () => {
|
||||||
const dbRequest = dbTeamRequests[0];
|
const dbRequest = dbTeamRequests[0];
|
||||||
const teamRequest = teamRequests[0];
|
const teamRequest = teamRequests[0];
|
||||||
|
|
||||||
@@ -536,6 +572,52 @@ describe('findRequestAndNextRequest', () => {
|
|||||||
|
|
||||||
expect(result).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND);
|
expect(result).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND);
|
||||||
});
|
});
|
||||||
|
test('should resolve left if the next request and given destCollId are different', () => {
|
||||||
|
const args: MoveTeamRequestArgs = {
|
||||||
|
srcCollID: teamRequests[0].collectionID,
|
||||||
|
destCollID: 'different_coll_id',
|
||||||
|
requestID: teamRequests[0].id,
|
||||||
|
nextRequestID: teamRequests[4].id,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockPrisma.teamRequest.findFirst
|
||||||
|
.mockResolvedValueOnce(dbTeamRequests[0])
|
||||||
|
.mockResolvedValueOnce(dbTeamRequests[4]);
|
||||||
|
|
||||||
|
const result = teamRequestService.findRequestAndNextRequest(
|
||||||
|
args.srcCollID,
|
||||||
|
args.requestID,
|
||||||
|
args.destCollID,
|
||||||
|
args.nextRequestID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).resolves.toEqualLeft(TEAM_REQ_INVALID_TARGET_COLL_ID);
|
||||||
|
});
|
||||||
|
test('should resolve left if the request and the next request are from different teams', async () => {
|
||||||
|
const args: MoveTeamRequestArgs = {
|
||||||
|
srcCollID: teamRequests[0].collectionID,
|
||||||
|
destCollID: teamRequests[4].collectionID,
|
||||||
|
requestID: teamRequests[0].id,
|
||||||
|
nextRequestID: teamRequests[4].id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
...dbTeamRequests[0],
|
||||||
|
teamID: 'different_team_id',
|
||||||
|
};
|
||||||
|
mockPrisma.teamRequest.findFirst
|
||||||
|
.mockResolvedValueOnce(request)
|
||||||
|
.mockResolvedValueOnce(dbTeamRequests[4]);
|
||||||
|
|
||||||
|
const result = await teamRequestService.findRequestAndNextRequest(
|
||||||
|
args.srcCollID,
|
||||||
|
args.requestID,
|
||||||
|
args.destCollID,
|
||||||
|
args.nextRequestID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(TEAM_REQ_INVALID_TARGET_COLL_ID);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('moveRequest', () => {
|
describe('moveRequest', () => {
|
||||||
@@ -725,13 +807,12 @@ describe('totalRequestsInATeam', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toEqual(0);
|
expect(result).toEqual(0);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
describe('getTeamRequestsCount', () => {
|
||||||
|
test('should return count of all Team Collections in the organization', async () => {
|
||||||
|
mockPrisma.teamRequest.count.mockResolvedValueOnce(10);
|
||||||
|
|
||||||
describe('getTeamRequestsCount', () => {
|
const result = await teamRequestService.getTeamRequestsCount();
|
||||||
test('should return count of all Team Collections in the organization', async () => {
|
expect(result).toEqual(10);
|
||||||
mockPrisma.teamRequest.count.mockResolvedValueOnce(10);
|
|
||||||
|
|
||||||
const result = await teamRequestService.getTeamRequestsCount();
|
|
||||||
expect(result).toEqual(10);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UserCollection } from '@prisma/client';
|
import { UserCollection, UserRequest as DbUserRequest } from '@prisma/client';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
USER_COLL_DEST_SAME,
|
USER_COLL_DEST_SAME,
|
||||||
@@ -11,12 +11,17 @@ import {
|
|||||||
USER_COLL_SHORT_TITLE,
|
USER_COLL_SHORT_TITLE,
|
||||||
USER_COLL_ALREADY_ROOT,
|
USER_COLL_ALREADY_ROOT,
|
||||||
USER_NOT_OWNER,
|
USER_NOT_OWNER,
|
||||||
|
USER_NOT_FOUND,
|
||||||
|
USER_COLL_INVALID_JSON,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { ReqType } from 'src/types/RequestTypes';
|
import { ReqType } from 'src/types/RequestTypes';
|
||||||
import { UserCollectionService } from './user-collection.service';
|
import { UserCollectionService } from './user-collection.service';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||||
|
import { UserCollectionExportJSONData } from './user-collections.model';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -341,11 +346,485 @@ const rootGQLGQLUserCollectionList: UserCollection[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const userRESTRequestList: DbUserRequest[] = [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
collectionID: rootRESTUserCollection.id,
|
||||||
|
userUid: user.uid,
|
||||||
|
title: 'Request 1',
|
||||||
|
request: {},
|
||||||
|
type: ReqType.REST,
|
||||||
|
orderIndex: 1,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('importCollectionsFromJSON', () => {
|
||||||
|
test('should resolve left for invalid JSON string', async () => {
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
'invalidJSONString',
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_COLL_INVALID_JSON));
|
||||||
|
});
|
||||||
|
test('should resolve left if JSON string is not an array', async () => {
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
JSON.stringify({}),
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(E.left(USER_COLL_INVALID_JSON));
|
||||||
|
});
|
||||||
|
test('should resolve left if destCollectionID is invalid', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.left(USER_COLL_NOT_FOUND));
|
||||||
|
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
JSON.stringify([]),
|
||||||
|
user.uid,
|
||||||
|
'invalidID',
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(E.left(USER_COLL_NOT_FOUND));
|
||||||
|
});
|
||||||
|
test('should resolve left if destCollectionID is not owned by this user', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||||
|
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
JSON.stringify([]),
|
||||||
|
'anotherUserUid',
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(E.left(USER_NOT_OWNER));
|
||||||
|
});
|
||||||
|
test('should resolve left if destCollection type miss match', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||||
|
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
JSON.stringify([]),
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.GQL,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(E.left(USER_COLL_NOT_SAME_TYPE));
|
||||||
|
});
|
||||||
|
test('should resolve right for valid JSON and destCollectionID provided', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||||
|
|
||||||
|
// private getChildCollectionsCount function call
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
mockPrisma.$transaction.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
JSON.stringify([]),
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(E.right(true));
|
||||||
|
});
|
||||||
|
test('should resolve right for importing in root directory (destCollectionID == null)', async () => {
|
||||||
|
// private getChildCollectionsCount function call
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
mockPrisma.$transaction.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
name: 'collection-name',
|
||||||
|
folders: [],
|
||||||
|
requests: [{ name: 'request-name' }],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
user.uid,
|
||||||
|
null,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(E.right(true));
|
||||||
|
});
|
||||||
|
test('should resolve right and publish event', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||||
|
|
||||||
|
// private getChildCollectionsCount function call
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
mockPrisma.$transaction.mockResolvedValueOnce([{}]);
|
||||||
|
|
||||||
|
const result = await userCollectionService.importCollectionsFromJSON(
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
name: 'collection-name',
|
||||||
|
folders: [],
|
||||||
|
requests: [{ name: 'request-name' }],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(E.right(true));
|
||||||
|
expect(mockPubSub.publish).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exportUserCollectionsToJSON', () => {
|
||||||
|
test('should return a list of user collections successfully for valid collectionID input and structure - 1', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection (id: 1 [exporting this collection])
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> <no request of root coll>
|
||||||
|
|-> <no request of root coll>
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childRESTUserCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: exportUserCollectionToJSONObject
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
mockPrisma.userRequest.findMany.mockResolvedValueOnce([]);
|
||||||
|
const returnFromCallee: CollectionFolder = {
|
||||||
|
id: childRESTUserCollection.id,
|
||||||
|
name: childRESTUserCollection.title,
|
||||||
|
folders: [],
|
||||||
|
requests: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Back to exportUserCollectionsToJSON
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||||
|
mockPrisma.userRequest.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const returnedValue: UserCollectionExportJSONData = {
|
||||||
|
exportedCollection: JSON.stringify({
|
||||||
|
id: rootRESTUserCollection.id,
|
||||||
|
name: rootRESTUserCollection.title,
|
||||||
|
folders: [returnFromCallee],
|
||||||
|
requests: [],
|
||||||
|
}),
|
||||||
|
collectionType: ReqType.REST,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await userCollectionService.exportUserCollectionsToJSON(
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(returnedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return a list of user collections successfully for valid collectionID input and structure - 2', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection (id: 1 [exporting this collection])
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> request1
|
||||||
|
|-> <no request of root coll>
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childRESTUserCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: exportUserCollectionToJSONObject
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
||||||
|
const returnFromCallee: CollectionFolder = {
|
||||||
|
id: childRESTUserCollection.id,
|
||||||
|
name: childRESTUserCollection.title,
|
||||||
|
folders: [],
|
||||||
|
requests: userRESTRequestList.map((r) => {
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
name: r.title,
|
||||||
|
...(r.request as Record<string, unknown>),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Back to exportUserCollectionsToJSON
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||||
|
mockPrisma.userRequest.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const returnedValue: UserCollectionExportJSONData = {
|
||||||
|
exportedCollection: JSON.stringify({
|
||||||
|
id: rootRESTUserCollection.id,
|
||||||
|
name: rootRESTUserCollection.title,
|
||||||
|
folders: [returnFromCallee],
|
||||||
|
requests: [],
|
||||||
|
}),
|
||||||
|
collectionType: ReqType.REST,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await userCollectionService.exportUserCollectionsToJSON(
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(returnedValue);
|
||||||
|
});
|
||||||
|
test('should return a list of user collections successfully for valid collectionID input and structure - 3', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection (id: 1 [exporting this collection])
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> request1
|
||||||
|
|-> request2
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childRESTUserCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: exportUserCollectionToJSONObject
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
||||||
|
const returnFromCallee: CollectionFolder = {
|
||||||
|
id: childRESTUserCollection.id,
|
||||||
|
name: childRESTUserCollection.title,
|
||||||
|
folders: [],
|
||||||
|
requests: userRESTRequestList.map((r) => {
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
name: r.title,
|
||||||
|
...(r.request as Record<string, unknown>),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Back to exportUserCollectionsToJSON
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
||||||
|
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
||||||
|
|
||||||
|
const returnedValue: UserCollectionExportJSONData = {
|
||||||
|
exportedCollection: JSON.stringify({
|
||||||
|
id: rootRESTUserCollection.id,
|
||||||
|
name: rootRESTUserCollection.title,
|
||||||
|
folders: [returnFromCallee],
|
||||||
|
requests: userRESTRequestList.map((x) => {
|
||||||
|
return {
|
||||||
|
id: x.id,
|
||||||
|
name: x.title,
|
||||||
|
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
collectionType: ReqType.REST,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await userCollectionService.exportUserCollectionsToJSON(
|
||||||
|
user.uid,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(returnedValue);
|
||||||
|
});
|
||||||
|
test('should return a list of user collections successfully for collectionID == null', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection (id: 1 [exporting this collection])
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> request1
|
||||||
|
|-> request2
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childRESTUserCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: exportUserCollectionToJSONObject
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
||||||
|
const returnFromCallee: CollectionFolder = {
|
||||||
|
id: childRESTUserCollection.id,
|
||||||
|
name: childRESTUserCollection.title,
|
||||||
|
folders: [],
|
||||||
|
requests: userRESTRequestList.map((r) => {
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
name: r.title,
|
||||||
|
...(r.request as Record<string, unknown>),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Back to exportUserCollectionsToJSON
|
||||||
|
|
||||||
|
const returnedValue: UserCollectionExportJSONData = {
|
||||||
|
exportedCollection: JSON.stringify([returnFromCallee]),
|
||||||
|
collectionType: ReqType.REST,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await userCollectionService.exportUserCollectionsToJSON(
|
||||||
|
user.uid,
|
||||||
|
null,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(returnedValue);
|
||||||
|
});
|
||||||
|
test('should return USER_COLL_NOT_FOUND if collectionID or its child not found in DB', async () => {
|
||||||
|
/*
|
||||||
|
Assuming collection and request structure is as follows:
|
||||||
|
|
||||||
|
rootTeamCollection (id: 1 [exporting this collection])
|
||||||
|
|-> childTeamCollection
|
||||||
|
| |-> request1 <NOT FOUND IN DATABASE>
|
||||||
|
|-> request2
|
||||||
|
*/
|
||||||
|
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
||||||
|
childRESTUserCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// RCV CALL 1: exportUserCollectionToJSONObject
|
||||||
|
jest
|
||||||
|
.spyOn(userCollectionService, 'getUserCollection')
|
||||||
|
.mockResolvedValueOnce(E.left(USER_COLL_NOT_FOUND));
|
||||||
|
|
||||||
|
// Back to exportUserCollectionsToJSON
|
||||||
|
|
||||||
|
const result = await userCollectionService.exportUserCollectionsToJSON(
|
||||||
|
user.uid,
|
||||||
|
null,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserOfCollection', () => {
|
||||||
|
test('should return a user successfully with valid collectionID', async () => {
|
||||||
|
mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({
|
||||||
|
...rootRESTUserCollection,
|
||||||
|
user: user,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await userCollectionService.getUserOfCollection(
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(user);
|
||||||
|
});
|
||||||
|
test('should return null with invalid collectionID', async () => {
|
||||||
|
mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValue('error');
|
||||||
|
|
||||||
|
const result = await userCollectionService.getUserOfCollection('invalidId');
|
||||||
|
expect(result).toEqualLeft(USER_NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserChildCollections', () => {
|
||||||
|
test('should return a list of child collections successfully with valid collectionID and userID', async () => {
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce(
|
||||||
|
childRESTUserCollectionList,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await userCollectionService.getUserChildCollections(
|
||||||
|
user,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
null,
|
||||||
|
10,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(childRESTUserCollectionList);
|
||||||
|
expect(mockPrisma.userCollection.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
userUid: user.uid,
|
||||||
|
parentID: rootRESTUserCollection.id,
|
||||||
|
type: ReqType.REST,
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
cursor: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('should return an empty list if no child collections found', async () => {
|
||||||
|
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await userCollectionService.getUserChildCollections(
|
||||||
|
user,
|
||||||
|
rootRESTUserCollection.id,
|
||||||
|
null,
|
||||||
|
10,
|
||||||
|
ReqType.REST,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(mockPrisma.userCollection.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
userUid: user.uid,
|
||||||
|
parentID: rootRESTUserCollection.id,
|
||||||
|
type: ReqType.REST,
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
skip: 0,
|
||||||
|
cursor: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCollectionCount', () => {
|
||||||
|
test('should return the count of collections', async () => {
|
||||||
|
const collectionID = 'collection123';
|
||||||
|
const count = 5;
|
||||||
|
|
||||||
|
mockPrisma.userCollection.count.mockResolvedValueOnce(count);
|
||||||
|
|
||||||
|
const result = await userCollectionService.getCollectionCount(collectionID);
|
||||||
|
|
||||||
|
expect(result).toEqual(count);
|
||||||
|
expect(mockPrisma.userCollection.count).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockPrisma.userCollection.count).toHaveBeenCalledWith({
|
||||||
|
where: { parentID: collectionID },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getParentOfUserCollection', () => {
|
describe('getParentOfUserCollection', () => {
|
||||||
test('should return a user-collection successfully with valid collectionID', async () => {
|
test('should return a user-collection successfully with valid collectionID', async () => {
|
||||||
mockPrisma.userCollection.findUnique.mockResolvedValueOnce({
|
mockPrisma.userCollection.findUnique.mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ beforeEach(() => {
|
|||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
describe('UserHistoryService', () => {
|
describe('UserHistoryService', () => {
|
||||||
describe('fetchUserHistory', () => {
|
describe('fetchUserHistory', () => {
|
||||||
test('Should return a list of users REST history if exists', async () => {
|
test('Should return a list of users REST history if exists', async () => {
|
||||||
@@ -142,13 +140,15 @@ describe('UserHistoryService', () => {
|
|||||||
});
|
});
|
||||||
describe('createUserHistory', () => {
|
describe('createUserHistory', () => {
|
||||||
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
|
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,13 +172,15 @@ describe('UserHistoryService', () => {
|
|||||||
).toEqualRight(userHistory);
|
).toEqualRight(userHistory);
|
||||||
});
|
});
|
||||||
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
|
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,7 +190,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,13 +214,15 @@ describe('UserHistoryService', () => {
|
|||||||
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
||||||
});
|
});
|
||||||
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,7 +232,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,13 +249,15 @@ describe('UserHistoryService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('Should create a REST request to users history and publish a created subscription', async () => {
|
test('Should create a REST request to users history and publish a created subscription', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,7 +267,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -402,7 +408,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: date,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -412,7 +418,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: date,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
|
USER_COLLECTION_NOT_FOUND,
|
||||||
|
USER_COLL_NOT_FOUND,
|
||||||
|
USER_REQUEST_INVALID_TYPE,
|
||||||
USER_REQUEST_NOT_FOUND,
|
USER_REQUEST_NOT_FOUND,
|
||||||
USER_REQUEST_REORDERING_FAILED,
|
USER_REQUEST_REORDERING_FAILED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
@@ -373,6 +376,101 @@ describe('UserRequestService', () => {
|
|||||||
|
|
||||||
expect(result).resolves.toEqualLeft(JSON_INVALID);
|
expect(result).resolves.toEqualLeft(JSON_INVALID);
|
||||||
});
|
});
|
||||||
|
test('Should resolve left for invalid collection ID', () => {
|
||||||
|
const args: CreateUserRequestArgs = {
|
||||||
|
collectionID: 'invalid-collection-id',
|
||||||
|
title: userRequests[0].title,
|
||||||
|
request: userRequests[0].request,
|
||||||
|
type: userRequests[0].type,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockPrisma.userRequest.count.mockResolvedValue(
|
||||||
|
dbUserRequests[0].orderIndex - 1,
|
||||||
|
);
|
||||||
|
mockUserCollectionService.getUserCollection.mockResolvedValue(
|
||||||
|
E.left(USER_COLL_NOT_FOUND),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = userRequestService.createRequest(
|
||||||
|
args.collectionID,
|
||||||
|
args.title,
|
||||||
|
args.request,
|
||||||
|
args.type,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).resolves.toEqualLeft(USER_COLL_NOT_FOUND);
|
||||||
|
});
|
||||||
|
test('Should resolve left for wrong collection ID (using other users collection ID)', () => {
|
||||||
|
const args: CreateUserRequestArgs = {
|
||||||
|
collectionID: userRequests[0].collectionID,
|
||||||
|
title: userRequests[0].title,
|
||||||
|
request: userRequests[0].request,
|
||||||
|
type: userRequests[0].type,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockPrisma.userRequest.count.mockResolvedValue(
|
||||||
|
dbUserRequests[0].orderIndex - 1,
|
||||||
|
);
|
||||||
|
mockUserCollectionService.getUserCollection.mockResolvedValue(
|
||||||
|
E.right({ type: userRequests[0].type, userUid: 'another-user' } as any),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = userRequestService.createRequest(
|
||||||
|
args.collectionID,
|
||||||
|
args.title,
|
||||||
|
args.request,
|
||||||
|
args.type,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).resolves.toEqualLeft(USER_COLLECTION_NOT_FOUND);
|
||||||
|
});
|
||||||
|
test('Should resolve left for collection type and request type miss match', () => {
|
||||||
|
const args: CreateUserRequestArgs = {
|
||||||
|
collectionID: userRequests[0].collectionID,
|
||||||
|
title: userRequests[0].title,
|
||||||
|
request: userRequests[0].request,
|
||||||
|
type: userRequests[0].type,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockUserCollectionService.getUserCollection.mockResolvedValue(
|
||||||
|
E.right({ type: 'invalid-type', userUid: user.uid } as any),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = userRequestService.createRequest(
|
||||||
|
args.collectionID,
|
||||||
|
args.title,
|
||||||
|
args.request,
|
||||||
|
args.type,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).resolves.toEqualLeft(USER_REQUEST_INVALID_TYPE);
|
||||||
|
});
|
||||||
|
test('Should resolve left if DB request type and parameter type is different', () => {
|
||||||
|
const args: CreateUserRequestArgs = {
|
||||||
|
collectionID: userRequests[0].collectionID,
|
||||||
|
title: userRequests[0].title,
|
||||||
|
request: userRequests[0].request,
|
||||||
|
type: userRequests[0].type,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockPrisma.userRequest.count.mockResolvedValue(
|
||||||
|
dbUserRequests[0].orderIndex - 1,
|
||||||
|
);
|
||||||
|
mockPrisma.userRequest.create.mockResolvedValue(dbUserRequests[0]);
|
||||||
|
|
||||||
|
const result = userRequestService.createRequest(
|
||||||
|
args.collectionID,
|
||||||
|
args.title,
|
||||||
|
args.request,
|
||||||
|
ReqType.GQL,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).resolves.toEqualLeft(USER_REQUEST_INVALID_TYPE);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateRequest', () => {
|
describe('updateRequest', () => {
|
||||||
|
|||||||
@@ -9,13 +9,7 @@ import * as E from 'fp-ts/Either';
|
|||||||
import * as A from 'fp-ts/Array';
|
import * as A from 'fp-ts/Array';
|
||||||
import { TeamMemberRole } from './team/team.model';
|
import { TeamMemberRole } from './team/team.model';
|
||||||
import { User } from './user/user.model';
|
import { User } from './user/user.model';
|
||||||
import {
|
import { JSON_INVALID } from './errors';
|
||||||
ENV_EMPTY_AUTH_PROVIDERS,
|
|
||||||
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
|
|
||||||
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
|
|
||||||
JSON_INVALID,
|
|
||||||
} from './errors';
|
|
||||||
import { AuthProvider } from './auth/helper';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -158,31 +152,3 @@ export function isValidLength(title: string, length: number) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called by bootstrap() in main.ts
|
|
||||||
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
|
||||||
* If not, it throws an error.
|
|
||||||
*/
|
|
||||||
export function checkEnvironmentAuthProvider() {
|
|
||||||
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
|
||||||
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
|
||||||
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
|
||||||
}
|
|
||||||
|
|
||||||
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
|
||||||
',',
|
|
||||||
).map((provider) => provider.toLocaleUpperCase());
|
|
||||||
const supportedAuthProviders = Object.values(AuthProvider).map(
|
|
||||||
(provider: string) => provider.toLocaleUpperCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const givenAuthProvider of givenAuthProviders) {
|
|
||||||
if (!supportedAuthProviders.includes(givenAuthProvider)) {
|
|
||||||
throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
|
jest: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
@@ -29,18 +30,8 @@ module.exports = {
|
|||||||
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||||
"prettier/prettier": [
|
"prettier/prettier":
|
||||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||||
{},
|
|
||||||
{
|
|
||||||
semi: false,
|
|
||||||
trailingComma: "es5",
|
|
||||||
singleQuote: false,
|
|
||||||
printWidth: 80,
|
|
||||||
useTabs: false,
|
|
||||||
tabWidth: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"vue/multi-word-component-names": "off",
|
"vue/multi-word-component-names": "off",
|
||||||
"vue/no-side-effects-in-computed-properties": "off",
|
"vue/no-side-effects-in-computed-properties": "off",
|
||||||
"import/no-named-as-default": "off",
|
"import/no-named-as-default": "off",
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
semi: false,
|
semi: false
|
||||||
trailingComma: "es5",
|
|
||||||
singleQuote: false,
|
|
||||||
printWidth: 80,
|
|
||||||
useTabs: false,
|
|
||||||
tabWidth: 2
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
@apply after:backface-hidden;
|
@apply after:backface-hidden;
|
||||||
@apply selection:bg-accentDark;
|
@apply selection:bg-accentDark;
|
||||||
@apply selection:text-accentContrast;
|
@apply selection:text-accentContrast;
|
||||||
@apply overscroll-none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -166,6 +165,12 @@ a {
|
|||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply sm:inline-flex;
|
@apply sm:inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-icon {
|
||||||
|
@apply transition;
|
||||||
|
@apply inline-flex;
|
||||||
|
@apply items-center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -184,11 +189,10 @@ a {
|
|||||||
@apply border-solid border-dividerDark;
|
@apply border-solid border-dividerDark;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply shadow-lg;
|
@apply shadow-lg;
|
||||||
@apply max-w-[45vw] #{!important};
|
|
||||||
|
|
||||||
.tippy-content {
|
.tippy-content {
|
||||||
@apply flex flex-col;
|
@apply flex flex-col;
|
||||||
@apply max-h-[45vh];
|
@apply max-h-56;
|
||||||
@apply items-stretch;
|
@apply items-stretch;
|
||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
@apply text-secondary text-body;
|
@apply text-secondary text-body;
|
||||||
@@ -196,10 +200,6 @@ a {
|
|||||||
@apply leading-normal;
|
@apply leading-normal;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
& > span {
|
|
||||||
@apply block #{!important};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -215,7 +215,6 @@ a {
|
|||||||
|
|
||||||
[data-v-tippy] {
|
[data-v-tippy] {
|
||||||
@apply flex flex-1;
|
@apply flex flex-1;
|
||||||
@apply truncate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[interactive] > div {
|
[interactive] > div {
|
||||||
@@ -326,7 +325,7 @@ pre.ace_editor {
|
|||||||
@apply after:font-icon;
|
@apply after:font-icon;
|
||||||
@apply after:text-current;
|
@apply after:text-current;
|
||||||
@apply after:right-3;
|
@apply after:right-3;
|
||||||
@apply after:content-["\e5cf"];
|
@apply after:content-["\e313"];
|
||||||
@apply after:text-lg;
|
@apply after:text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,10 +480,6 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-scroller {
|
|
||||||
@apply overscroll-y-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
.cm-line::selection {
|
.cm-line::selection {
|
||||||
@apply bg-accentDark #{!important};
|
@apply bg-accentDark #{!important};
|
||||||
@@ -572,11 +567,3 @@ details[open] summary .indicator {
|
|||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply border-0;
|
@apply border-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gql-operation-not-highlight {
|
|
||||||
@apply opacity-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gql-operation-highlight {
|
|
||||||
@apply opacity-100;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@mixin base-theme {
|
@mixin base-theme {
|
||||||
--font-sans: "Inter Variable", sans-serif;
|
--font-sans: "Inter", sans-serif;
|
||||||
--font-icon: "Material Symbols Rounded Variable";
|
--font-mono: "Roboto Mono", monospace;
|
||||||
--font-mono: "Roboto Mono Variable", monospace;
|
--font-icon: "Material Icons";
|
||||||
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"choose_file": "Choose a file",
|
"choose_file": "Choose a file",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_history": "Clear All History",
|
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
@@ -31,7 +30,6 @@
|
|||||||
"open_workspace": "Open workspace",
|
"open_workspace": "Open workspace",
|
||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"prettify": "Prettify",
|
"prettify": "Prettify",
|
||||||
"rename": "Rename",
|
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -69,8 +67,6 @@
|
|||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
|
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
|
||||||
"invite_your_friends": "Invite your friends",
|
"invite_your_friends": "Invite your friends",
|
||||||
"social_links": "Social links",
|
|
||||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
|
||||||
"join_discord_community": "Join our Discord community",
|
"join_discord_community": "Join our Discord community",
|
||||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"name": "Hoppscotch",
|
"name": "Hoppscotch",
|
||||||
@@ -135,7 +131,6 @@
|
|||||||
"renamed": "Collection renamed",
|
"renamed": "Collection renamed",
|
||||||
"request_in_use": "Request in use",
|
"request_in_use": "Request in use",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
"save_to_collection": "Save to Collection",
|
|
||||||
"select": "Select a Collection",
|
"select": "Select a Collection",
|
||||||
"select_location": "Select location",
|
"select_location": "Select location",
|
||||||
"select_team": "Select a team",
|
"select_team": "Select a team",
|
||||||
@@ -153,15 +148,8 @@
|
|||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
|
||||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
|
||||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||||
},
|
},
|
||||||
"context_menu": {
|
|
||||||
"set_environment_variable": "Set as variable",
|
|
||||||
"add_parameters": "Add to parameters",
|
|
||||||
"open_request_in_new_tab": "Open request in new tab"
|
|
||||||
},
|
|
||||||
"count": {
|
"count": {
|
||||||
"header": "Header {count}",
|
"header": "Header {count}",
|
||||||
"message": "Message {count}",
|
"message": "Message {count}",
|
||||||
@@ -204,31 +192,17 @@
|
|||||||
"create_new": "Create new environment",
|
"create_new": "Create new environment",
|
||||||
"created": "Environment created",
|
"created": "Environment created",
|
||||||
"deleted": "Environment deletion",
|
"deleted": "Environment deletion",
|
||||||
"duplicated": "Environment duplicated",
|
|
||||||
"edit": "Edit Environment",
|
"edit": "Edit Environment",
|
||||||
"global": "Global",
|
|
||||||
"empty_variables": "No variables",
|
|
||||||
"global_variables": "Global variables",
|
|
||||||
"invalid_name": "Please provide a name for the environment",
|
"invalid_name": "Please provide a name for the environment",
|
||||||
"list": "Environment variables",
|
|
||||||
"my_environments": "My Environments",
|
"my_environments": "My Environments",
|
||||||
"name": "Name",
|
|
||||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||||
"new": "New Environment",
|
"new": "New Environment",
|
||||||
"no_active_environment": "No active environment",
|
|
||||||
"no_environment": "No environment",
|
"no_environment": "No environment",
|
||||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||||
"quick_peek": "Environment Quick Peek",
|
|
||||||
"replace_with_variable": "Replace with variable",
|
|
||||||
"scope": "Scope",
|
|
||||||
"select": "Select environment",
|
"select": "Select environment",
|
||||||
"set": "Set environment",
|
|
||||||
"set_as_environment": "Set as environment",
|
|
||||||
"team_environments": "Team Environments",
|
"team_environments": "Team Environments",
|
||||||
"title": "Environments",
|
"title": "Environments",
|
||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
"value": "Value",
|
|
||||||
"variable": "Variable",
|
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -252,7 +226,6 @@
|
|||||||
"no_duration": "No duration",
|
"no_duration": "No duration",
|
||||||
"no_results_found": "No matches found",
|
"no_results_found": "No matches found",
|
||||||
"page_not_found": "This page could not be found",
|
"page_not_found": "This page could not be found",
|
||||||
"proxy_error": "Proxy error",
|
|
||||||
"script_fail": "Could not execute pre-request script",
|
"script_fail": "Could not execute pre-request script",
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
"test_script_fail": "Could not execute post-request script"
|
"test_script_fail": "Could not execute post-request script"
|
||||||
@@ -280,10 +253,6 @@
|
|||||||
"graphql": {
|
"graphql": {
|
||||||
"mutations": "Mutations",
|
"mutations": "Mutations",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
"switch_connection": "Switch connection",
|
|
||||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
|
||||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
|
||||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
|
||||||
"subscriptions": "Subscriptions"
|
"subscriptions": "Subscriptions"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
@@ -313,30 +282,6 @@
|
|||||||
"preview": "Hide Preview",
|
"preview": "Hide Preview",
|
||||||
"sidebar": "Collapse sidebar"
|
"sidebar": "Collapse sidebar"
|
||||||
},
|
},
|
||||||
"inspections": {
|
|
||||||
"title": "Inspector",
|
|
||||||
"description": "Inspect possible errors",
|
|
||||||
"environment": {
|
|
||||||
"add_environment": "Add to Environment",
|
|
||||||
"not_found": "Environment variable “{environment}” not found."
|
|
||||||
},
|
|
||||||
"header": {
|
|
||||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"401_error": "Please check your authentication credentials.",
|
|
||||||
"404_error": "Please check your request URL and method type.",
|
|
||||||
"network_error": "Please check your network connection.",
|
|
||||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
|
||||||
"default_error": "Please check your request."
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"extension_not_installed": "Extension not installed.",
|
|
||||||
"extention_not_enabled": "Extension not enabled.",
|
|
||||||
"extention_enable_action": "Enable Browser Extension",
|
|
||||||
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"import": {
|
"import": {
|
||||||
"collections": "Import collections",
|
"collections": "Import collections",
|
||||||
"curl": "Import cURL",
|
"curl": "Import cURL",
|
||||||
@@ -473,10 +418,8 @@
|
|||||||
"payload": "Payload",
|
"payload": "Payload",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"raw_body": "Raw Request Body",
|
"raw_body": "Raw Request Body",
|
||||||
"rename": "Rename Request",
|
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"stop": "Stop",
|
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
"saved": "Request saved",
|
"saved": "Request saved",
|
||||||
@@ -516,9 +459,9 @@
|
|||||||
"account_name_description": "This is your display name.",
|
"account_name_description": "This is your display name.",
|
||||||
"background": "Background",
|
"background": "Background",
|
||||||
"black_mode": "Black",
|
"black_mode": "Black",
|
||||||
"dark_mode": "Dark",
|
|
||||||
"change_font_size": "Change font size",
|
"change_font_size": "Change font size",
|
||||||
"choose_language": "Choose language",
|
"choose_language": "Choose language",
|
||||||
|
"dark_mode": "Dark",
|
||||||
"delete_account": "Delete account",
|
"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_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||||
"expand_navigation": "Expand navigation",
|
"expand_navigation": "Expand navigation",
|
||||||
@@ -582,10 +525,6 @@
|
|||||||
"show_all": "Keyboard shortcuts",
|
"show_all": "Keyboard shortcuts",
|
||||||
"title": "General"
|
"title": "General"
|
||||||
},
|
},
|
||||||
"others": {
|
|
||||||
"title": "Others",
|
|
||||||
"prettify": "Prettify Editor's Content"
|
|
||||||
},
|
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
"invite": "Invite people to Hoppscotch",
|
"invite": "Invite people to Hoppscotch",
|
||||||
"title": "Miscellaneous"
|
"title": "Miscellaneous"
|
||||||
@@ -606,9 +545,6 @@
|
|||||||
"delete_method": "Select DELETE method",
|
"delete_method": "Select DELETE method",
|
||||||
"get_method": "Select GET method",
|
"get_method": "Select GET method",
|
||||||
"head_method": "Select HEAD method",
|
"head_method": "Select HEAD method",
|
||||||
"rename": "Rename Request",
|
|
||||||
"import_curl": "Import cURL",
|
|
||||||
"show_code": "Generate code snippet",
|
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
"next_method": "Select Next method",
|
"next_method": "Select Next method",
|
||||||
"post_method": "Select POST method",
|
"post_method": "Select POST method",
|
||||||
@@ -617,7 +553,6 @@
|
|||||||
"reset_request": "Reset Request",
|
"reset_request": "Reset Request",
|
||||||
"save_to_collections": "Save to Collections",
|
"save_to_collections": "Save to Collections",
|
||||||
"send_request": "Send Request",
|
"send_request": "Send Request",
|
||||||
"save_request": "Save Request",
|
|
||||||
"title": "Request"
|
"title": "Request"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
@@ -626,10 +561,10 @@
|
|||||||
"title": "Response"
|
"title": "Response"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Switch theme to Black Mode",
|
"black": "Switch theme to black mode",
|
||||||
"dark": "Switch theme to Dark Mode",
|
"dark": "Switch theme to dark mode",
|
||||||
"light": "Switch theme to Light Mode",
|
"light": "Switch theme to light mode",
|
||||||
"system": "Switch theme to System Mode",
|
"system": "Switch theme to system mode",
|
||||||
"title": "Theme"
|
"title": "Theme"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -647,83 +582,6 @@
|
|||||||
"log": "Log",
|
"log": "Log",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"spotlight": {
|
|
||||||
"general": {
|
|
||||||
"help_menu": "Help and support",
|
|
||||||
"chat": "Chat with support",
|
|
||||||
"open_docs": "Read Documentation",
|
|
||||||
"open_keybindings": "Keyboard shortcuts",
|
|
||||||
"social": "Social",
|
|
||||||
"title": "General"
|
|
||||||
},
|
|
||||||
"miscellaneous": {
|
|
||||||
"invite": "Invite people to Hoppscotch",
|
|
||||||
"title": "Miscellaneous"
|
|
||||||
},
|
|
||||||
"request": {
|
|
||||||
"switch_to": "Switch to",
|
|
||||||
"select_method": "Select method",
|
|
||||||
"save_as_new": "Save as new request",
|
|
||||||
"tab_parameters": "Parameters tab",
|
|
||||||
"tab_body": "Body tab",
|
|
||||||
"tab_headers": "Headers tab",
|
|
||||||
"tab_authorization": "Authorization tab",
|
|
||||||
"tab_pre_request_script": "Pre-request script tab",
|
|
||||||
"tab_tests": "Tests tab"
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"copy": "Copy response",
|
|
||||||
"download": "Download response as file",
|
|
||||||
"title": "Response"
|
|
||||||
},
|
|
||||||
"environments": {
|
|
||||||
"new": "Create new environment",
|
|
||||||
"new_variable": "Create a new environment variable",
|
|
||||||
"edit": "Edit selected environment",
|
|
||||||
"delete": "Delete selected environment",
|
|
||||||
"duplicate": "Duplicate selected environment",
|
|
||||||
"edit_global": "Edit global environment",
|
|
||||||
"duplicate_global": "Duplicate global environment",
|
|
||||||
"title": "Environments"
|
|
||||||
},
|
|
||||||
"workspace": {
|
|
||||||
"new": "Create new team",
|
|
||||||
"edit": "Edit selected team",
|
|
||||||
"delete": "Delete selected team",
|
|
||||||
"invite": "Invite people to team",
|
|
||||||
"switch_to_personal": "Switch to personal workspace",
|
|
||||||
"title": "Teams"
|
|
||||||
},
|
|
||||||
"tab": {
|
|
||||||
"duplicate": "Duplicate tab",
|
|
||||||
"close_current": "Close current tab",
|
|
||||||
"close_others": "Close other tabs",
|
|
||||||
"new_tab": "Open a new tab",
|
|
||||||
"title": "Tabs"
|
|
||||||
},
|
|
||||||
"section": {
|
|
||||||
"user": "User",
|
|
||||||
"theme": "Theme",
|
|
||||||
"interface": "Interface",
|
|
||||||
"interceptor": "Interceptor"
|
|
||||||
},
|
|
||||||
"change_language": "Change Language",
|
|
||||||
"settings": {
|
|
||||||
"theme": {
|
|
||||||
"black": "Black Mode",
|
|
||||||
"dark": "Dark Mode",
|
|
||||||
"light": "Light Mode",
|
|
||||||
"system": "System Mode"
|
|
||||||
},
|
|
||||||
"font": {
|
|
||||||
"size_sm": "Change to Small",
|
|
||||||
"size_md": "Change to Medium",
|
|
||||||
"size_lg": "Change to Large"
|
|
||||||
},
|
|
||||||
"change_interceptor": "Change Interceptor",
|
|
||||||
"change_language": "Change Language"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sse": {
|
"sse": {
|
||||||
"event_type": "Event type",
|
"event_type": "Event type",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
@@ -781,11 +639,8 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "Authorization",
|
"authorization": "Authorization",
|
||||||
"body": "Body",
|
"body": "Body",
|
||||||
"close": "Close Tab",
|
|
||||||
"close_others": "Close other Tabs",
|
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
"duplicate": "Duplicate Tab",
|
|
||||||
"environments": "Environments",
|
"environments": "Environments",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.8",
|
"version": "2023.4.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"test": "vitest --run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
||||||
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||||
@@ -15,147 +13,140 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
||||||
"postinstall": "pnpm run gql-codegen",
|
"postinstall": "pnpm run gql-codegen",
|
||||||
"do-test": "pnpm run test",
|
|
||||||
"do-lint": "pnpm run prod-lint",
|
"do-lint": "pnpm run prod-lint",
|
||||||
"do-typecheck": "pnpm run lint",
|
"do-typecheck": "pnpm run lint",
|
||||||
"do-lintfix": "pnpm run lintfix"
|
"do-lintfix": "pnpm run lintfix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.0",
|
"@apidevtools/swagger-parser": "^10.1.0",
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.0.3",
|
||||||
"@codemirror/commands": "^6.2.4",
|
"@codemirror/commands": "^6.0.1",
|
||||||
"@codemirror/lang-javascript": "^6.1.9",
|
"@codemirror/lang-javascript": "^6.0.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.0",
|
||||||
"@codemirror/lang-xml": "^6.0.2",
|
"@codemirror/lang-xml": "^6.0.0",
|
||||||
"@codemirror/language": "^6.9.0",
|
"@codemirror/language": "^6.2.0",
|
||||||
"@codemirror/legacy-modes": "^6.3.3",
|
"@codemirror/legacy-modes": "^6.1.0",
|
||||||
"@codemirror/lint": "^6.4.0",
|
"@codemirror/lint": "^6.0.0",
|
||||||
"@codemirror/search": "^6.5.1",
|
"@codemirror/search": "^6.0.0",
|
||||||
"@codemirror/state": "^6.2.1",
|
"@codemirror/state": "^6.1.0",
|
||||||
"@codemirror/view": "^6.16.0",
|
"@codemirror/view": "^6.0.2",
|
||||||
"@fontsource-variable/inter": "^5.0.8",
|
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
|
||||||
"@fontsource-variable/roboto-mono": "^5.0.9",
|
|
||||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@hoppscotch/ui": "workspace:^",
|
"@hoppscotch/ui": "workspace:^",
|
||||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||||
"@lezer/highlight": "^1.1.6",
|
"@lezer/highlight": "^1.0.0",
|
||||||
"@sentry/tracing": "^7.64.0",
|
"@sentry/tracing": "^7.13.0",
|
||||||
"@sentry/vue": "^7.64.0",
|
"@sentry/vue": "^7.13.0",
|
||||||
"@urql/core": "^4.1.1",
|
"@urql/core": "^2.5.0",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-auth": "^2.1.6",
|
"@urql/exchange-auth": "^0.1.7",
|
||||||
"@urql/exchange-graphcache": "^6.3.2",
|
"@urql/exchange-graphcache": "^4.4.3",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "^2.3.0",
|
||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^8.7.5",
|
||||||
"@vueuse/head": "^1.3.1",
|
"@vueuse/head": "^0.7.9",
|
||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^0.21.4",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"dioc": "workspace:^",
|
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "^2.12.1",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"globalthis": "^1.0.3",
|
"globalthis": "^1.0.3",
|
||||||
"graphql": "^16.8.0",
|
"graphql": "^15.5.0",
|
||||||
"graphql-language-service-interface": "^2.9.1",
|
"graphql-language-service-interface": "^2.9.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"httpsnippet": "^2.0.0",
|
"httpsnippet": "^2.0.0",
|
||||||
"insomnia-importers": "^3.6.0",
|
"insomnia-importers": "^3.3.0",
|
||||||
"io-ts": "^2.2.20",
|
"io-ts": "^2.2.16",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonpath-plus": "^7.2.0",
|
"jsonpath-plus": "^7.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lossless-json": "^2.0.11",
|
"lossless-json": "^2.0.8",
|
||||||
"minisearch": "^6.1.0",
|
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "^1.1.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postman-collection": "^4.2.0",
|
"postman-collection": "^4.1.4",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.10.3",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.5.5",
|
||||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
||||||
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
||||||
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
||||||
"socketio-wildcard": "^2.0.0",
|
"socketio-wildcard": "^2.0.0",
|
||||||
"splitpanes": "^3.1.5",
|
"splitpanes": "^3.1.1",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"tern": "^0.24.3",
|
"tern": "^0.24.3",
|
||||||
"timers": "^0.1.1",
|
"timers": "^0.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"url": "^0.11.1",
|
"url": "^0.11.0",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^8.3.2",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.2.25",
|
||||||
|
"vue-github-button": "^3.0.3",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-pdf-embed": "^1.1.6",
|
"vue-pdf-embed": "^1.1.4",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.0.16",
|
||||||
"vue-tippy": "6.3.1",
|
"vue-tippy": "6.0.0-alpha.58",
|
||||||
"vuedraggable-es": "^4.1.1",
|
"vuedraggable-es": "^4.1.1",
|
||||||
"wonka": "^6.3.4",
|
"wonka": "^4.0.15",
|
||||||
"workbox-window": "^7.0.0",
|
"workbox-window": "^6.5.4",
|
||||||
"xml-formatter": "^3.5.0",
|
"xml-formatter": "^3.4.1",
|
||||||
"yargs-parser": "^21.1.1"
|
"yargs-parser": "^21.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||||
"@graphql-codegen/add": "^5.0.0",
|
"@graphql-codegen/add": "^3.2.0",
|
||||||
"@graphql-codegen/cli": "^5.0.0",
|
"@graphql-codegen/cli": "^2.8.0",
|
||||||
"@graphql-codegen/typed-document-node": "^5.0.1",
|
"@graphql-codegen/typed-document-node": "^2.3.1",
|
||||||
"@graphql-codegen/typescript": "^4.0.1",
|
"@graphql-codegen/typescript": "^2.7.1",
|
||||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
"@graphql-codegen/typescript-operations": "^2.5.1",
|
||||||
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
|
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1",
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
"@graphql-codegen/urql-introspection": "^2.2.0",
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
"@graphql-typed-document-node/core": "^3.1.1",
|
||||||
"@iconify-json/lucide": "^1.1.119",
|
"@iconify-json/lucide": "^1.1.40",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@rushstack/eslint-patch": "^1.1.4",
|
||||||
"@rushstack/eslint-patch": "^1.3.3",
|
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/lodash-es": "^4.17.8",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/lossless-json": "^1.0.1",
|
"@types/lossless-json": "^1.0.1",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/paho-mqtt": "^1.0.7",
|
"@types/paho-mqtt": "^1.0.6",
|
||||||
"@types/postman-collection": "^3.5.7",
|
"@types/postman-collection": "^3.5.7",
|
||||||
"@types/splitpanes": "^2.2.1",
|
"@types/splitpanes": "^2.2.1",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^8.3.4",
|
||||||
"@types/yargs-parser": "^21.0.0",
|
"@types/yargs-parser": "^21.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||||
"@typescript-eslint/parser": "^6.4.0",
|
"@typescript-eslint/parser": "^5.19.0",
|
||||||
"@vitejs/plugin-vue": "^4.3.1",
|
"@vitejs/plugin-vue": "^3.1.0",
|
||||||
"@vue/compiler-sfc": "^3.3.4",
|
"@vue/compiler-sfc": "^3.2.39",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.1",
|
||||||
"@vue/runtime-core": "^3.3.4",
|
"@vue/runtime-core": "^3.2.39",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.24.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.5.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.0.0",
|
||||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
"rollup-plugin-polyfill-node": "^0.10.1",
|
||||||
"sass": "^1.66.0",
|
"sass": "^1.53.0",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^4.5.4",
|
||||||
"unplugin-fonts": "^1.0.3",
|
"unplugin-icons": "^0.14.9",
|
||||||
"unplugin-icons": "^0.16.5",
|
"unplugin-vue-components": "^0.21.0",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"vite": "^3.1.4",
|
||||||
"vite": "^4.4.9",
|
"vite-plugin-checker": "^0.5.1",
|
||||||
"vite-plugin-checker": "^0.6.1",
|
"vite-plugin-fonts": "^0.6.0",
|
||||||
"vite-plugin-html-config": "^1.0.11",
|
"vite-plugin-html-config": "^1.0.10",
|
||||||
"vite-plugin-inspect": "^0.7.38",
|
"vite-plugin-inspect": "^0.7.4",
|
||||||
"vite-plugin-pages": "^0.31.0",
|
"vite-plugin-pages": "^0.26.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
"vite-plugin-pages-sitemap": "^1.4.0",
|
||||||
"vite-plugin-pwa": "^0.16.4",
|
"vite-plugin-pwa": "^0.13.1",
|
||||||
"vite-plugin-vue-layouts": "^0.8.0",
|
"vite-plugin-vue-layouts": "^0.7.0",
|
||||||
"vite-plugin-windicss": "^1.9.1",
|
"vite-plugin-windicss": "^1.8.8",
|
||||||
"vitest": "^0.34.2",
|
"vue-tsc": "^0.38.2",
|
||||||
"vue-tsc": "^1.8.8",
|
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#6366f1" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#fff" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>
|
|
||||||
|
Before Width: | Height: | Size: 389 B |
50
packages/hoppscotch-common/src/components.d.ts
vendored
50
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,37 +1,30 @@
|
|||||||
/* eslint-disable */
|
// generated by unplugin-vue-components
|
||||||
/* prettier-ignore */
|
// We suggest you to commit this file into source control
|
||||||
// @ts-nocheck
|
|
||||||
// Generated by unplugin-vue-components
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core'
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||||
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
|
||||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||||
|
AppFuse: typeof import('./components/app/Fuse.vue')['default']
|
||||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
||||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||||
AppInspection: typeof import('./components/app/Inspection.vue')['default']
|
|
||||||
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
||||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||||
AppOptions: typeof import('./components/app/Options.vue')['default']
|
AppOptions: typeof import('./components/app/Options.vue')['default']
|
||||||
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
||||||
|
AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
|
||||||
|
AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
|
||||||
AppShare: typeof import('./components/app/Share.vue')['default']
|
AppShare: typeof import('./components/app/Share.vue')['default']
|
||||||
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
||||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
||||||
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
||||||
AppSocial: typeof import('./components/app/Social.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']
|
|
||||||
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
|
|
||||||
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
|
|
||||||
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
|
|
||||||
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
|
|
||||||
AppSupport: typeof import('./components/app/Support.vue')['default']
|
AppSupport: typeof import('./components/app/Support.vue')['default']
|
||||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
||||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
||||||
@@ -60,7 +53,6 @@ declare module 'vue' {
|
|||||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
Environments: typeof import('./components/environments/index.vue')['default']
|
Environments: typeof import('./components/environments/index.vue')['default']
|
||||||
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
|
||||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||||
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
||||||
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
||||||
@@ -73,18 +65,12 @@ declare module 'vue' {
|
|||||||
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
|
||||||
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
|
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
|
||||||
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
|
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
|
||||||
GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
|
|
||||||
GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
|
|
||||||
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
|
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
|
||||||
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
||||||
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
|
|
||||||
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
||||||
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
||||||
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
|
|
||||||
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
|
|
||||||
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
||||||
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
||||||
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
|
|
||||||
History: typeof import('./components/history/index.vue')['default']
|
History: typeof import('./components/history/index.vue')['default']
|
||||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||||
@@ -96,22 +82,17 @@ declare module 'vue' {
|
|||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
|
||||||
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||||
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
||||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
|
||||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||||
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
|
||||||
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
|
|
||||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||||
@@ -133,17 +114,14 @@ declare module 'vue' {
|
|||||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||||
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
||||||
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
|
|
||||||
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
|
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
|
||||||
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
||||||
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
||||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||||
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
|
||||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
|
||||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||||
@@ -153,11 +131,8 @@ declare module 'vue' {
|
|||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
|
||||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||||
@@ -177,8 +152,6 @@ declare module 'vue' {
|
|||||||
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
||||||
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
||||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||||
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
|
||||||
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
|
||||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||||
@@ -190,13 +163,11 @@ declare module 'vue' {
|
|||||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
||||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
||||||
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
||||||
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
|
|
||||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
||||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||||
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
||||||
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
|
|
||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
@@ -205,8 +176,8 @@ declare module 'vue' {
|
|||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||||
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
||||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
||||||
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
|
SmartTree: typeof import('./components/smart/Tree.vue')['default']
|
||||||
SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.vue')['default']
|
SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default']
|
||||||
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
|
||||||
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
|
||||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||||
@@ -222,4 +193,5 @@ declare module 'vue' {
|
|||||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,54 +2,16 @@
|
|||||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||||
|
|
||||||
<HoppSmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="t('confirm.remove_team')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="deleteTeam()"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { pipe } from "fp-ts/function"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
|
||||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
|
||||||
import { showChat } from "~/modules/crisp"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const showShortcuts = ref(false)
|
const showShortcuts = ref(false)
|
||||||
const showShare = ref(false)
|
const showShare = ref(false)
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
|
|
||||||
const confirmRemove = ref(false)
|
|
||||||
|
|
||||||
const teamID = ref<string | null>(null)
|
|
||||||
|
|
||||||
const deleteTeam = () => {
|
|
||||||
if (!teamID.value) return
|
|
||||||
pipe(
|
|
||||||
backendDeleteTeam(teamID.value),
|
|
||||||
TE.match(
|
|
||||||
(err) => {
|
|
||||||
// TODO: Better errors ? We know the possible errors now
|
|
||||||
toast.error(`${t("error.something_went_wrong")}`)
|
|
||||||
console.error(err)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
invokeAction("workspace.switch.personal")
|
|
||||||
toast.success(`${t("team.deleted")}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)() // Tasks (and TEs) are lazy, so call the function returned
|
|
||||||
}
|
|
||||||
|
|
||||||
defineActionHandler("flyouts.keybinds.toggle", () => {
|
defineActionHandler("flyouts.keybinds.toggle", () => {
|
||||||
showShortcuts.value = !showShortcuts.value
|
showShortcuts.value = !showShortcuts.value
|
||||||
})
|
})
|
||||||
@@ -61,13 +23,4 @@ defineActionHandler("modals.share.toggle", () => {
|
|||||||
defineActionHandler("modals.login.toggle", () => {
|
defineActionHandler("modals.login.toggle", () => {
|
||||||
showLogin.value = !showLogin.value
|
showLogin.value = !showLogin.value
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("flyouts.chat.open", () => {
|
|
||||||
showChat()
|
|
||||||
})
|
|
||||||
|
|
||||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
|
||||||
teamID.value = teamId
|
|
||||||
confirmRemove.value = true
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="contextMenuRef"
|
|
||||||
class="fixed bg-popover shadow-lg transform translate-y-8 border border-dividerDark p-2 rounded"
|
|
||||||
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
|
|
||||||
>
|
|
||||||
<div v-if="contextMenuOptions" class="flex flex-col">
|
|
||||||
<div
|
|
||||||
v-for="option in contextMenuOptions"
|
|
||||||
:key="option.id"
|
|
||||||
class="flex flex-col space-y-2"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
v-if="option.text.type === 'text' && option.text"
|
|
||||||
:icon="option.icon"
|
|
||||||
:label="option.text.text"
|
|
||||||
@click="handleClick(option)"
|
|
||||||
/>
|
|
||||||
<component
|
|
||||||
:is="option.text.component"
|
|
||||||
v-else-if="option.text.type === 'custom'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onClickOutside } from "@vueuse/core"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { ref, watch } from "vue"
|
|
||||||
import { ContextMenuResult, ContextMenuService } from "~/services/context-menu"
|
|
||||||
import { EnvironmentMenuService } from "~/services/context-menu/menu/environment.menu"
|
|
||||||
import { ParameterMenuService } from "~/services/context-menu/menu/parameter.menu"
|
|
||||||
import { URLMenuService } from "~/services/context-menu/menu/url.menu"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
show: boolean
|
|
||||||
position: { top: number; left: number }
|
|
||||||
text: string | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const contextMenuRef = ref<any | null>(null)
|
|
||||||
|
|
||||||
const contextMenuOptions = ref<ContextMenuResult[]>([])
|
|
||||||
|
|
||||||
onClickOutside(contextMenuRef, () => {
|
|
||||||
emit("hide-modal")
|
|
||||||
})
|
|
||||||
|
|
||||||
const contextMenuService = useService(ContextMenuService)
|
|
||||||
|
|
||||||
useService(EnvironmentMenuService)
|
|
||||||
useService(ParameterMenuService)
|
|
||||||
useService(URLMenuService)
|
|
||||||
|
|
||||||
const handleClick = (option: { action: () => void }) => {
|
|
||||||
option.action()
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [props.show, props.text],
|
|
||||||
(val) => {
|
|
||||||
if (val && props.text) {
|
|
||||||
const options = contextMenuService.getMenuFor(props.text)
|
|
||||||
contextMenuOptions.value = options
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
:title="`${t(
|
:title="`${t(
|
||||||
'app.shortcuts'
|
'app.shortcuts'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>/</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>K</kbd>`"
|
||||||
:icon="IconZap"
|
:icon="IconZap"
|
||||||
@click="invokeAction('flyouts.keybinds.toggle')"
|
@click="invokeAction('flyouts.keybinds.toggle')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
70
packages/hoppscotch-common/src/components/app/Fuse.vue
Normal file
70
packages/hoppscotch-common/src/components/app/Fuse.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div key="outputHash" class="flex flex-col flex-1 overflow-auto">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<AppPowerSearchEntry
|
||||||
|
v-for="(shortcut, shortcutIndex) in searchResults"
|
||||||
|
:key="`shortcut-${shortcutIndex}`"
|
||||||
|
:active="shortcutIndex === selectedEntry"
|
||||||
|
:shortcut="shortcut.item"
|
||||||
|
@action="emit('action', shortcut.item.action)"
|
||||||
|
@mouseover="selectedEntry = shortcutIndex"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="searchResults.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
|
<span class="my-2 text-center">
|
||||||
|
{{ t("state.nothing_found") }} "{{ search }}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onUnmounted, onMounted } from "vue"
|
||||||
|
import Fuse from "fuse.js"
|
||||||
|
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||||
|
import { HoppAction } from "~/helpers/actions"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
input: Record<string, any>[]
|
||||||
|
search: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "action", action: HoppAction): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
keys: ["keys", "label", "action", "tags"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuse = new Fuse(props.input, options)
|
||||||
|
|
||||||
|
const searchResults = computed(() => fuse.search(props.search))
|
||||||
|
|
||||||
|
const searchResultsItems = computed(() =>
|
||||||
|
searchResults.value.map((searchResult) => searchResult.item)
|
||||||
|
)
|
||||||
|
|
||||||
|
const emitSearchAction = (action: HoppAction) => emit("action", action)
|
||||||
|
|
||||||
|
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
|
||||||
|
useArrowKeysNavigation(searchResultsItems, {
|
||||||
|
onEnter: emitSearchAction,
|
||||||
|
stopPropagation: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bindArrowKeysListeners()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unbindArrowKeysListeners()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -15,21 +15,16 @@
|
|||||||
:label="t('app.name')"
|
:label="t('app.name')"
|
||||||
to="/"
|
to="/"
|
||||||
/>
|
/>
|
||||||
|
<!-- <AppGitHubStarButton class="mt-1.5 transition" /> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center justify-center flex-1 space-x-2">
|
<div class="inline-flex items-center space-x-2">
|
||||||
<button
|
<HoppButtonSecondary
|
||||||
class="flex flex-1 items-center justify-between px-2 py-1 self-stretch bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-60 hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
|
:title="`${t('app.search')} <kbd>/</kbd>`"
|
||||||
|
:icon="IconSearch"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle')"
|
||||||
>
|
/>
|
||||||
<span class="inline-flex flex-1 items-center">
|
|
||||||
<icon-lucide-search class="mr-2 svg-icons" />
|
|
||||||
{{ t("app.search") }}
|
|
||||||
</span>
|
|
||||||
<span class="flex space-x-1">
|
|
||||||
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
|
|
||||||
<kbd class="shortcut-key">K</kbd>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="showInstallButton"
|
v-if="showInstallButton"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -47,8 +42,6 @@
|
|||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
@click="invokeAction('modals.support.toggle')"
|
@click="invokeAction('modals.support.toggle')"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="inline-flex items-center justify-end flex-1 space-x-2">
|
|
||||||
<div
|
<div
|
||||||
v-if="currentUser === null"
|
v-if="currentUser === null"
|
||||||
class="inline-flex items-center space-x-2"
|
class="inline-flex items-center space-x-2"
|
||||||
@@ -243,21 +236,19 @@ import IconDownload from "~icons/lucide/download"
|
|||||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
|
import IconSearch from "~icons/lucide/search"
|
||||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
import { invokeAction } from "@helpers/actions"
|
||||||
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
||||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||||
import { onLoggedIn } from "~/composables/auth"
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Once the PWA code is initialized, this holds a method
|
* Once the PWA code is initialized, this holds a method
|
||||||
@@ -374,8 +365,6 @@ const handleTeamEdit = () => {
|
|||||||
editingTeamID.value = workspace.value.teamID
|
editingTeamID.value = workspace.value.teamID
|
||||||
editingTeamName.value = { name: selectedTeam.value.name }
|
editingTeamName.value = { name: selectedTeam.value.name }
|
||||||
displayModalEdit(true)
|
displayModalEdit(true)
|
||||||
} else {
|
|
||||||
noPermission()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,29 +374,4 @@ const profile = ref<any | null>(null)
|
|||||||
const settings = ref<any | null>(null)
|
const settings = ref<any | null>(null)
|
||||||
const logout = ref<any | null>(null)
|
const logout = ref<any | null>(null)
|
||||||
const accountActions = ref<any | null>(null)
|
const accountActions = ref<any | null>(null)
|
||||||
|
|
||||||
defineActionHandler("modals.team.edit", handleTeamEdit)
|
|
||||||
|
|
||||||
defineActionHandler("modals.team.invite", () => {
|
|
||||||
if (
|
|
||||||
selectedTeam.value?.myRole === "OWNER" ||
|
|
||||||
selectedTeam.value?.myRole === "EDITOR"
|
|
||||||
) {
|
|
||||||
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
|
|
||||||
} else {
|
|
||||||
noPermission()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
defineActionHandler(
|
|
||||||
"user.login",
|
|
||||||
() => {
|
|
||||||
invokeAction("modals.login.toggle")
|
|
||||||
},
|
|
||||||
computed(() => !currentUser.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
const noPermission = () => {
|
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="inspectionResults && inspectionResults.length > 0">
|
|
||||||
<tippy interactive trigger="click" theme="popover">
|
|
||||||
<div class="flex justify-center items-center flex-1 flex-col">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconAlertTriangle"
|
|
||||||
:class="severityColor(getHighestSeverity.severity)"
|
|
||||||
:title="t('inspections.description')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div class="flex flex-col space-y-2 items-start flex-1">
|
|
||||||
<div
|
|
||||||
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
|
|
||||||
>
|
|
||||||
<span class="flex items-center flex-1">
|
|
||||||
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
|
|
||||||
<span class="font-bold">
|
|
||||||
{{ t("inspections.title") }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
to="https://docs.hoppscotch.io/documentation/features/inspections"
|
|
||||||
blank
|
|
||||||
:title="t('app.wiki')"
|
|
||||||
:icon="IconHelpCircle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="(inspector, index) in inspectionResults"
|
|
||||||
:key="index"
|
|
||||||
class="flex self-stretch max-w-md w-full"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="inspector.text.type === 'text'"
|
|
||||||
class="flex-1 px-3 py-2"
|
|
||||||
>
|
|
||||||
{{ inspector.text.text }}
|
|
||||||
<HoppSmartLink
|
|
||||||
blank
|
|
||||||
:to="inspector.doc.link"
|
|
||||||
class="text-accent hover:text-accentDark transition"
|
|
||||||
>
|
|
||||||
{{ inspector.doc.text }}
|
|
||||||
<icon-lucide-arrow-up-right class="svg-icons" />
|
|
||||||
</HoppSmartLink>
|
|
||||||
</span>
|
|
||||||
<span v-if="inspector.action" class="flex p-2 space-x-2">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="inspector.action.text"
|
|
||||||
outline
|
|
||||||
filled
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
inspector.action?.apply()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { InspectorResult } from "~/services/inspection"
|
|
||||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
|
||||||
import { computed } from "vue"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
inspectionResults: InspectorResult[] | undefined
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const getHighestSeverity = computed(() => {
|
|
||||||
if (props.inspectionResults) {
|
|
||||||
return props.inspectionResults.reduce(
|
|
||||||
(prev, curr) => {
|
|
||||||
return prev.severity > curr.severity ? prev : curr
|
|
||||||
},
|
|
||||||
{ severity: 0 }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return { severity: 0 }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const severityColor = (severity: number) => {
|
|
||||||
switch (severity) {
|
|
||||||
case 1:
|
|
||||||
return "!text-green-500 hover:!text-green-600"
|
|
||||||
case 2:
|
|
||||||
return "!text-yellow-500 hover:!text-yellow-600"
|
|
||||||
case 3:
|
|
||||||
return "!text-red-500 hover:!text-red-600"
|
|
||||||
default:
|
|
||||||
return "!text-gray-500 hover:!text-gray-600"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -8,41 +8,91 @@
|
|||||||
{{ t("settings.interceptor_description") }}
|
{{ t("settings.interceptor_description") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<HoppSmartRadioGroup
|
||||||
<div>
|
v-model="interceptorSelection"
|
||||||
<div
|
:radios="interceptors"
|
||||||
v-for="interceptor in interceptors"
|
/>
|
||||||
:key="interceptor.interceptorID"
|
<div
|
||||||
class="flex flex-col"
|
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion"
|
||||||
>
|
class="flex space-x-2"
|
||||||
<HoppSmartRadio
|
>
|
||||||
:value="interceptor.interceptorID"
|
<HoppButtonSecondary
|
||||||
:label="unref(interceptor.name(t))"
|
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||||
:selected="interceptorSelection === interceptor.interceptorID"
|
blank
|
||||||
@change="interceptorSelection = interceptor.interceptorID"
|
:icon="IconChrome"
|
||||||
/>
|
label="Chrome"
|
||||||
|
outline
|
||||||
<component
|
class="!flex-1"
|
||||||
:is="interceptor.selectorSubtitle"
|
/>
|
||||||
v-if="interceptor.selectorSubtitle"
|
<HoppButtonSecondary
|
||||||
/>
|
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||||
</div>
|
blank
|
||||||
|
:icon="IconFirefox"
|
||||||
|
label="Firefox"
|
||||||
|
outline
|
||||||
|
class="!flex-1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import IconChrome from "~icons/brands/chrome"
|
||||||
|
import IconFirefox from "~icons/brands/firefox"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { applySetting, toggleSetting } from "~/newstore/settings"
|
||||||
|
import { useSetting } from "@composables/settings"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useService } from "dioc/vue"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { Ref, unref } from "vue"
|
import { extensionStatus$ } from "~/newstore/HoppExtension"
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const interceptorService = useService(InterceptorService)
|
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
|
||||||
|
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
|
||||||
|
|
||||||
const interceptorSelection =
|
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
|
||||||
interceptorService.currentInterceptorID as Ref<string>
|
|
||||||
|
|
||||||
const interceptors = interceptorService.availableInterceptors
|
const extensionVersion = computed(() => {
|
||||||
|
return currentExtensionStatus.value === "available"
|
||||||
|
? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null
|
||||||
|
: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const interceptors = computed(() => [
|
||||||
|
{ value: "BROWSER_ENABLED" as const, label: t("state.none") },
|
||||||
|
{ value: "PROXY_ENABLED" as const, label: t("settings.proxy") },
|
||||||
|
{
|
||||||
|
value: "EXTENSIONS_ENABLED" as const,
|
||||||
|
label:
|
||||||
|
`${t("settings.extensions")}: ` +
|
||||||
|
(extensionVersion.value !== null
|
||||||
|
? `v${extensionVersion.value.major}.${extensionVersion.value.minor}`
|
||||||
|
: t("settings.extension_ver_not_reported")),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
type InterceptorMode = (typeof interceptors)["value"][number]["value"]
|
||||||
|
|
||||||
|
const interceptorSelection = computed<InterceptorMode>({
|
||||||
|
get() {
|
||||||
|
if (PROXY_ENABLED.value) return "PROXY_ENABLED"
|
||||||
|
if (EXTENSIONS_ENABLED.value) return "EXTENSIONS_ENABLED"
|
||||||
|
return "BROWSER_ENABLED"
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
if (val === "EXTENSIONS_ENABLED") {
|
||||||
|
applySetting("EXTENSIONS_ENABLED", true)
|
||||||
|
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
|
||||||
|
}
|
||||||
|
if (val === "PROXY_ENABLED") {
|
||||||
|
applySetting("PROXY_ENABLED", true)
|
||||||
|
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
|
||||||
|
}
|
||||||
|
if (val === "BROWSER_ENABLED") {
|
||||||
|
applySetting("PROXY_ENABLED", false)
|
||||||
|
applySetting("EXTENSIONS_ENABLED", false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -130,12 +130,13 @@
|
|||||||
@click="nativeShare()"
|
@click="nativeShare()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartModal>
|
</HoppSmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import IconSidebar from "~icons/lucide/sidebar"
|
import IconSidebar from "~icons/lucide/sidebar"
|
||||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||||
import IconBook from "~icons/lucide/book"
|
import IconBook from "~icons/lucide/book"
|
||||||
@@ -150,12 +151,13 @@ import IconUserPlus from "~icons/lucide/user-plus"
|
|||||||
import IconShare2 from "~icons/lucide/share-2"
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import IconChevronRight from "~icons/lucide/chevron-right"
|
import IconChevronRight from "~icons/lucide/chevron-right"
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { showChat } from "@modules/crisp"
|
import { showChat } from "@modules/crisp"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const navigatorShare = !!navigator.share
|
const navigatorShare = !!navigator.share
|
||||||
|
const showShare = ref(false)
|
||||||
|
|
||||||
const ZEN_MODE = useSetting("ZEN_MODE")
|
const ZEN_MODE = useSetting("ZEN_MODE")
|
||||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||||
@@ -172,6 +174,10 @@ defineProps<{
|
|||||||
show: boolean
|
show: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
defineActionHandler("modals.share.toggle", () => {
|
||||||
|
showShare.value = !showShare.value
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
@@ -192,7 +198,7 @@ const expandCollection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expandInvite = () => {
|
const expandInvite = () => {
|
||||||
invokeAction("modals.share.toggle")
|
showShare.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeShare = () => {
|
const nativeShare = () => {
|
||||||
|
|||||||
@@ -18,18 +18,13 @@
|
|||||||
:horizontal="COLUMN_LAYOUT"
|
:horizontal="COLUMN_LAYOUT"
|
||||||
@resize="setPaneEvent($event, 'horizontal')"
|
@resize="setPaneEvent($event, 'horizontal')"
|
||||||
>
|
>
|
||||||
<Pane
|
<Pane :size="PANE_MAIN_TOP_SIZE" class="flex flex-col !overflow-auto">
|
||||||
:size="PANE_MAIN_TOP_SIZE"
|
|
||||||
class="flex flex-col !overflow-auto"
|
|
||||||
min-size="25"
|
|
||||||
>
|
|
||||||
<slot name="primary" />
|
<slot name="primary" />
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane
|
<Pane
|
||||||
v-if="hasSecondary"
|
v-if="hasSecondary"
|
||||||
:size="PANE_MAIN_BOTTOM_SIZE"
|
:size="PANE_MAIN_BOTTOM_SIZE"
|
||||||
class="flex flex-col !overflow-auto"
|
class="flex flex-col !overflow-auto"
|
||||||
min-size="25"
|
|
||||||
>
|
>
|
||||||
<slot name="secondary" />
|
<slot name="secondary" />
|
||||||
</Pane>
|
</Pane>
|
||||||
@@ -38,7 +33,7 @@
|
|||||||
<Pane
|
<Pane
|
||||||
v-if="SIDEBAR && hasSidebar"
|
v-if="SIDEBAR && hasSidebar"
|
||||||
:size="PANE_SIDEBAR_SIZE"
|
:size="PANE_SIDEBAR_SIZE"
|
||||||
min-size="25"
|
min-size="20"
|
||||||
class="flex flex-col !overflow-auto bg-primaryContrast"
|
class="flex flex-col !overflow-auto bg-primaryContrast"
|
||||||
>
|
>
|
||||||
<slot name="sidebar" />
|
<slot name="sidebar" />
|
||||||
@@ -83,10 +78,10 @@ type PaneEvent = {
|
|||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const PANE_MAIN_SIZE = ref(70)
|
const PANE_MAIN_SIZE = ref(74)
|
||||||
const PANE_SIDEBAR_SIZE = ref(30)
|
const PANE_SIDEBAR_SIZE = ref(26)
|
||||||
const PANE_MAIN_TOP_SIZE = ref(35)
|
const PANE_MAIN_TOP_SIZE = ref(42)
|
||||||
const PANE_MAIN_BOTTOM_SIZE = ref(65)
|
const PANE_MAIN_BOTTOM_SIZE = ref(58)
|
||||||
|
|
||||||
if (!COLUMN_LAYOUT.value) {
|
if (!COLUMN_LAYOUT.value) {
|
||||||
PANE_MAIN_TOP_SIZE.value = 50
|
PANE_MAIN_TOP_SIZE.value = 50
|
||||||
|
|||||||
122
packages/hoppscotch-common/src/components/app/PowerSearch.vue
Normal file
122
packages/hoppscotch-common/src/components/app/PowerSearch.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<HoppSmartModal
|
||||||
|
v-if="show"
|
||||||
|
styles="sm:max-w-lg"
|
||||||
|
full-width
|
||||||
|
@close="emit('hide-modal')"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="flex flex-col border-b transition border-dividerLight">
|
||||||
|
<input
|
||||||
|
id="command"
|
||||||
|
v-model="search"
|
||||||
|
v-focus
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
name="command"
|
||||||
|
:placeholder="`${t('app.type_a_command_search')}`"
|
||||||
|
class="flex flex-shrink-0 p-6 text-base bg-transparent text-secondaryDark"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex flex-shrink-0 text-tiny text-secondaryLight px-4 pb-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<kbd class="shortcut-key">↑</kbd>
|
||||||
|
<kbd class="shortcut-key">↓</kbd>
|
||||||
|
<span class="ml-2 truncate">
|
||||||
|
{{ t("action.to_navigate") }}
|
||||||
|
</span>
|
||||||
|
<kbd class="shortcut-key">↩</kbd>
|
||||||
|
<span class="ml-2 truncate">
|
||||||
|
{{ t("action.to_select") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<kbd class="shortcut-key">ESC</kbd>
|
||||||
|
<span class="ml-2 truncate">
|
||||||
|
{{ t("action.to_close") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AppFuse
|
||||||
|
v-if="search && show"
|
||||||
|
:input="fuse"
|
||||||
|
:search="search"
|
||||||
|
@action="runAction"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col flex-1 overflow-auto space-y-4 divide-y divide-dividerLight"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(map, mapIndex) in mappings"
|
||||||
|
:key="`map-${mapIndex}`"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
|
<h5 class="px-6 py-2 my-2 text-secondaryLight">
|
||||||
|
{{ t(map.section) }}
|
||||||
|
</h5>
|
||||||
|
<AppPowerSearchEntry
|
||||||
|
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
||||||
|
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
||||||
|
:shortcut="shortcut"
|
||||||
|
:active="shortcutsItems.indexOf(shortcut) === selectedEntry"
|
||||||
|
@action="runAction"
|
||||||
|
@mouseover="selectedEntry = shortcutsItems.indexOf(shortcut)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoppSmartModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from "vue"
|
||||||
|
import { HoppAction, invokeAction } from "~/helpers/actions"
|
||||||
|
import { spotlight as mappings, fuse } from "@helpers/shortcuts"
|
||||||
|
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const search = ref("")
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
search.value = ""
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
|
||||||
|
const runAction = (command: HoppAction) => {
|
||||||
|
invokeAction(command)
|
||||||
|
hideModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcutsItems = computed(() =>
|
||||||
|
mappings.reduce(
|
||||||
|
(shortcuts, section) => [...shortcuts, ...section.shortcuts],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
|
||||||
|
useArrowKeysNavigation(shortcutsItems, {
|
||||||
|
onEnter: runAction,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(show) => {
|
||||||
|
if (show) bindArrowKeysListeners()
|
||||||
|
else unbindArrowKeysListeners()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="flex items-center flex-1 px-6 py-3 font-medium transition cursor-pointer search-entry focus:outline-none"
|
||||||
|
:class="{ active: active }"
|
||||||
|
tabindex="-1"
|
||||||
|
@click="emit('action', shortcut.action)"
|
||||||
|
@keydown.enter="emit('action', shortcut.action)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="shortcut.icon"
|
||||||
|
class="mr-4 transition opacity-50 svg-icons"
|
||||||
|
:class="{ 'opacity-100 text-secondaryDark': active }"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="flex flex-1 mr-4 transition"
|
||||||
|
:class="{ 'text-secondaryDark': active }"
|
||||||
|
>
|
||||||
|
{{ t(shortcut.label) }}
|
||||||
|
</span>
|
||||||
|
<kbd
|
||||||
|
v-for="(key, keyIndex) in shortcut.keys"
|
||||||
|
:key="`key-${String(keyIndex)}`"
|
||||||
|
class="shortcut-key"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</kbd>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from "vue"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
shortcut: {
|
||||||
|
label: string
|
||||||
|
keys: string[]
|
||||||
|
action: string
|
||||||
|
icon: object | Component
|
||||||
|
}
|
||||||
|
active: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "action", action: string): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-entry {
|
||||||
|
@apply relative;
|
||||||
|
@apply after:absolute;
|
||||||
|
@apply after:top-0;
|
||||||
|
@apply after:left-0;
|
||||||
|
@apply after:bottom-0;
|
||||||
|
@apply after:bg-transparent;
|
||||||
|
@apply after:z-2;
|
||||||
|
@apply after:w-0.5;
|
||||||
|
@apply after:content-DEFAULT;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply bg-primaryLight;
|
||||||
|
@apply after:bg-accentLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,26 +4,20 @@
|
|||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
||||||
>
|
>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col px-6 py-4 border-b border-dividerLight">
|
||||||
v-model="filterText"
|
<input
|
||||||
type="search"
|
v-model="filterText"
|
||||||
styles="px-6 py-4 border-b border-dividerLight"
|
type="search"
|
||||||
:placeholder="`${t('action.search')}`"
|
autocomplete="off"
|
||||||
input-styles="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
|
class="flex px-4 py-2 border rounded bg-primaryContrast border-divider hover:border-dividerDark focus-visible:border-dividerDark"
|
||||||
/>
|
:placeholder="`${t('action.search')}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col divide-y divide-dividerLight">
|
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight">
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="isEmpty(shortcutsResults)"
|
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
|
||||||
>
|
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
|
|
||||||
<details
|
<details
|
||||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
v-for="(map, mapIndex) in searchResults"
|
||||||
v-else
|
:key="`map-${mapIndex}`"
|
||||||
:key="`section-${sectionTitle}`"
|
|
||||||
class="flex flex-col"
|
class="flex flex-col"
|
||||||
open
|
open
|
||||||
>
|
>
|
||||||
@@ -34,28 +28,63 @@
|
|||||||
<span
|
<span
|
||||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||||
>
|
>
|
||||||
{{ sectionTitle }}
|
{{ t(map.item.section) }}
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
<div class="flex flex-col px-6 pb-4 space-y-2">
|
||||||
<AppShortcutsEntry
|
<AppShortcutsEntry
|
||||||
v-for="(shortcut, index) in sectionResults"
|
v-for="(shortcut, index) in map.item.shortcuts"
|
||||||
:key="`shortcut-${index}`"
|
:key="`shortcut-${index}`"
|
||||||
:shortcut="shortcut"
|
:shortcut="shortcut"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<div
|
||||||
|
v-if="searchResults.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
|
<span class="my-2 text-center flex flex-col">
|
||||||
|
{{ t("state.nothing_found") }}
|
||||||
|
<span class="break-all">"{{ filterText }}"</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col divide-y divide-dividerLight">
|
||||||
|
<details
|
||||||
|
v-for="(map, mapIndex) in mappings"
|
||||||
|
:key="`map-${mapIndex}`"
|
||||||
|
class="flex flex-col"
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
class="flex items-center flex-1 min-w-0 px-6 py-4 font-semibold transition cursor-pointer focus:outline-none text-secondaryLight hover:text-secondaryDark"
|
||||||
|
>
|
||||||
|
<icon-lucide-chevron-right class="mr-2 indicator" />
|
||||||
|
<span
|
||||||
|
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||||
|
>
|
||||||
|
{{ t(map.section) }}
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="flex flex-col px-6 pb-4 space-y-2">
|
||||||
|
<AppShortcutsEntry
|
||||||
|
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
||||||
|
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
||||||
|
:shortcut="shortcut"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartSlideOver>
|
</HoppSmartSlideOver>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeMount, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { ShortcutDef, getShortcuts } from "~/helpers/shortcuts"
|
import Fuse from "fuse.js"
|
||||||
import MiniSearch from "minisearch"
|
import mappings from "~/helpers/shortcuts"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { groupBy, isEmpty } from "lodash-es"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -63,33 +92,15 @@ defineProps<{
|
|||||||
show: boolean
|
show: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const minisearch = new MiniSearch({
|
const options = {
|
||||||
fields: ["label", "keys", "section"],
|
keys: ["shortcuts.label"],
|
||||||
idField: "label",
|
}
|
||||||
storeFields: ["label", "keys", "section"],
|
|
||||||
searchOptions: {
|
|
||||||
fuzzy: true,
|
|
||||||
prefix: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const shortcuts = getShortcuts(t)
|
const fuse = new Fuse(mappings, options)
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
minisearch.addAllAsync(shortcuts)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filterText = ref("")
|
const filterText = ref("")
|
||||||
|
|
||||||
const shortcutsResults = computed(() => {
|
const searchResults = computed(() => fuse.search(filterText.value))
|
||||||
// If there are no search text, return all the shortcuts
|
|
||||||
const results =
|
|
||||||
filterText.value.length > 0
|
|
||||||
? minisearch.search(filterText.value)
|
|
||||||
: shortcuts
|
|
||||||
|
|
||||||
return groupBy(results, "section") as Record<string, ShortcutDef[]>
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void
|
(e: "close"): void
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center py-1">
|
<div class="flex items-center py-1">
|
||||||
<span class="flex flex-1 mr-4">
|
<span class="flex flex-1 mr-4">
|
||||||
{{ shortcut.label }}
|
{{ t(shortcut.label) }}
|
||||||
</span>
|
</span>
|
||||||
<kbd
|
<kbd
|
||||||
v-for="(key, index) in shortcut.keys"
|
v-for="(key, index) in shortcut.keys"
|
||||||
@@ -14,9 +14,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ShortcutDef } from "~/helpers/shortcuts"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
shortcut: ShortcutDef
|
shortcut: {
|
||||||
|
label: string
|
||||||
|
keys: string[]
|
||||||
|
}
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
||||||
<div class="flex mb-4 space-x-2">
|
<div class="flex pb-4 my-4 space-x-2">
|
||||||
<div class="flex flex-col items-end space-y-4 text-right">
|
<div class="flex flex-col items-end space-y-4 text-right">
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex items-center flex-1">
|
||||||
{{ t("shortcut.request.send_request") }}
|
{{ t("shortcut.request.send_request") }}
|
||||||
@@ -22,11 +22,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
<kbd class="shortcut-key">/</kbd>
|
<kbd class="shortcut-key">K</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
<kbd class="shortcut-key">/</kbd>
|
||||||
<kbd class="shortcut-key">K</kbd>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">?</kbd>
|
<kbd class="shortcut-key">?</kbd>
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button
|
|
||||||
ref="el"
|
|
||||||
class="flex items-center flex-1 px-6 py-4 font-medium space-x-4 transition cursor-pointer relative search-entry focus:outline-none"
|
|
||||||
:class="{ 'active bg-primaryLight text-secondaryDark': active }"
|
|
||||||
tabindex="-1"
|
|
||||||
@click="emit('action')"
|
|
||||||
@keydown.enter="emit('action')"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="entry.icon"
|
|
||||||
class="opacity-50 svg-icons"
|
|
||||||
:class="{ 'opacity-100': active }"
|
|
||||||
/>
|
|
||||||
<template
|
|
||||||
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
|
||||||
>
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ entry.text.text }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else-if="entry.text.type === 'text' && Array.isArray(entry.text.text)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="(labelPart, labelPartIndex) in entry.text.text"
|
|
||||||
:key="`label-${labelPart}-${labelPartIndex}`"
|
|
||||||
>
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ labelPart }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right
|
|
||||||
v-if="labelPartIndex < entry.text.text.length - 1"
|
|
||||||
class="flex flex-shrink-0"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="entry.text.type === 'custom'">
|
|
||||||
<span class="block truncate">
|
|
||||||
<component
|
|
||||||
:is="entry.text.component"
|
|
||||||
v-bind="entry.text.componentProps"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<span v-if="formattedShortcutKeys" class="block truncate">
|
|
||||||
<kbd
|
|
||||||
v-for="(key, keyIndex) in formattedShortcutKeys"
|
|
||||||
:key="`key-${String(keyIndex)}`"
|
|
||||||
class="shortcut-key"
|
|
||||||
>
|
|
||||||
{{ key }}
|
|
||||||
</kbd>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
|
||||||
import { SpotlightSearcherResult } from "~/services/spotlight"
|
|
||||||
|
|
||||||
const SPECIAL_KEY_CHARS: Record<string, string> = {
|
|
||||||
ctrl: getPlatformSpecialKey(),
|
|
||||||
alt: getPlatformAlternateKey(),
|
|
||||||
up: "↑",
|
|
||||||
down: "↓",
|
|
||||||
enter: "↩",
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, watch, ref } from "vue"
|
|
||||||
import { capitalize } from "lodash-es"
|
|
||||||
import { getPlatformAlternateKey } from "~/helpers/platformutils"
|
|
||||||
|
|
||||||
const el = ref<HTMLElement>()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
entry: SpotlightSearcherResult
|
|
||||||
active: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const formattedShortcutKeys = computed(
|
|
||||||
() =>
|
|
||||||
props.entry.meta?.keyboardShortcut?.map((key) => {
|
|
||||||
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "action"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.active,
|
|
||||||
(active) => {
|
|
||||||
if (active) {
|
|
||||||
el.value?.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "nearest",
|
|
||||||
inline: "nearest",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.search-entry {
|
|
||||||
@apply after:absolute;
|
|
||||||
@apply after:top-0;
|
|
||||||
@apply after:left-0;
|
|
||||||
@apply after:bottom-0;
|
|
||||||
@apply after:bg-transparent;
|
|
||||||
@apply after:z-2;
|
|
||||||
@apply after:w-0.5;
|
|
||||||
@apply after:content-DEFAULT;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
@apply after:bg-accentLight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="flex flex-1 items-center space-x-2">
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ dateTimeText }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ historyEntry.request.url }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
|
||||||
>
|
|
||||||
{{ historyEntry.request.query.split("\n")[0] }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from "vue"
|
|
||||||
import { shortDateTime } from "~/helpers/utils/date"
|
|
||||||
import { GQLHistoryEntry } from "~/newstore/history"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
historyEntry: GQLHistoryEntry
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const dateTimeText = computed(() =>
|
|
||||||
shortDateTime(props.historyEntry.updatedOn!)
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="flex flex-1 space-x-2 items-center">
|
|
||||||
<template v-for="(folder, index) in pathFolders" :key="index">
|
|
||||||
<span class="block" :class="{ truncate: index !== 0 }">
|
|
||||||
{{ folder.name }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
|
||||||
</template>
|
|
||||||
<span v-if="request" class="block">
|
|
||||||
{{ request.name }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
|
||||||
import { computed } from "vue"
|
|
||||||
import { graphqlCollectionStore } from "~/newstore/collections"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
folderPath: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const pathFolders = computed(() => {
|
|
||||||
try {
|
|
||||||
const folderIndicies = props.folderPath
|
|
||||||
.split("/")
|
|
||||||
.slice(0, -1)
|
|
||||||
.map((x) => parseInt(x))
|
|
||||||
|
|
||||||
const pathItems: HoppCollection<HoppGQLRequest>[] = []
|
|
||||||
|
|
||||||
let currentFolder =
|
|
||||||
graphqlCollectionStore.value.state[folderIndicies.shift()!]
|
|
||||||
|
|
||||||
pathItems.push(currentFolder)
|
|
||||||
|
|
||||||
while (folderIndicies.length > 0) {
|
|
||||||
const folderIndex = folderIndicies.shift()!
|
|
||||||
|
|
||||||
const folder = currentFolder.folders[folderIndex]
|
|
||||||
pathItems.push(folder)
|
|
||||||
|
|
||||||
currentFolder = folder
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathItems
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const request = computed(() => {
|
|
||||||
try {
|
|
||||||
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
|
|
||||||
|
|
||||||
return pathFolders.value[pathFolders.value.length - 1].requests[
|
|
||||||
requestIndex
|
|
||||||
]
|
|
||||||
} catch (e) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<IconLucideCheckCircle class="text-accent" />
|
|
||||||
</template>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="flex flex-1 items-center space-x-2">
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ dateTimeText }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
|
||||||
<span
|
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
|
||||||
:class="entryStatus.className"
|
|
||||||
>
|
|
||||||
{{ historyEntry.request.method }}
|
|
||||||
</span>
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ historyEntry.request.endpoint }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from "vue"
|
|
||||||
import findStatusGroup from "~/helpers/findStatusGroup"
|
|
||||||
import { shortDateTime } from "~/helpers/utils/date"
|
|
||||||
import { RESTHistoryEntry } from "~/newstore/history"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
historyEntry: RESTHistoryEntry
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const dateTimeText = computed(() =>
|
|
||||||
shortDateTime(props.historyEntry.updatedOn!)
|
|
||||||
)
|
|
||||||
|
|
||||||
const entryStatus = computed(() => {
|
|
||||||
const foundStatusGroup = findStatusGroup(
|
|
||||||
props.historyEntry.responseMeta.statusCode
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
foundStatusGroup || {
|
|
||||||
className: "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="flex flex-1 items-center space-x-2">
|
|
||||||
<template v-for="(folder, index) in pathFolders" :key="index">
|
|
||||||
<span class="block" :class="{ truncate: index !== 0 }">
|
|
||||||
{{ folder.name }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
|
||||||
</template>
|
|
||||||
<span
|
|
||||||
v-if="request"
|
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
|
||||||
:class="getMethodLabelColorClassOf(request)"
|
|
||||||
>
|
|
||||||
{{ request.method.toUpperCase() }}
|
|
||||||
</span>
|
|
||||||
<span v-if="request" class="block">
|
|
||||||
{{ request.name }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
|
||||||
import { computed } from "vue"
|
|
||||||
import { restCollectionStore } from "~/newstore/collections"
|
|
||||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
folderPath: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const pathFolders = computed(() => {
|
|
||||||
try {
|
|
||||||
const folderIndicies = props.folderPath
|
|
||||||
.split("/")
|
|
||||||
.slice(0, -1)
|
|
||||||
.map((x) => parseInt(x))
|
|
||||||
|
|
||||||
const pathItems: HoppCollection<HoppRESTRequest>[] = []
|
|
||||||
|
|
||||||
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
|
|
||||||
pathItems.push(currentFolder)
|
|
||||||
|
|
||||||
while (folderIndicies.length > 0) {
|
|
||||||
const folderIndex = folderIndicies.shift()!
|
|
||||||
|
|
||||||
const folder = currentFolder.folders[folderIndex]
|
|
||||||
pathItems.push(folder)
|
|
||||||
|
|
||||||
currentFolder = folder
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathItems
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const request = computed(() => {
|
|
||||||
try {
|
|
||||||
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
|
|
||||||
|
|
||||||
return pathFolders.value[pathFolders.value.length - 1].requests[
|
|
||||||
requestIndex
|
|
||||||
]
|
|
||||||
} catch (e) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
<template>
|
|
||||||
<HoppSmartModal
|
|
||||||
v-if="show"
|
|
||||||
styles="sm:max-w-lg"
|
|
||||||
full-width
|
|
||||||
@close="emit('hide-modal')"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<div class="flex flex-col border-b transition border-divider">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
id="command"
|
|
||||||
v-model="search"
|
|
||||||
v-focus
|
|
||||||
type="text"
|
|
||||||
autocomplete="off"
|
|
||||||
name="command"
|
|
||||||
:placeholder="`${t('app.type_a_command_search')}`"
|
|
||||||
class="flex flex-1 text-base bg-transparent text-secondaryDark px-6 py-5"
|
|
||||||
/>
|
|
||||||
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="searchSession && search.length > 0"
|
|
||||||
class="flex flex-col flex-1 overflow-y-auto border-b border-divider divide-y divide-dividerLight"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
|
|
||||||
:key="`section-${sectionID}`"
|
|
||||||
class="flex flex-col"
|
|
||||||
>
|
|
||||||
<h5
|
|
||||||
class="px-6 py-2 bg-primaryContrast z-10 text-secondaryLight sticky top-0"
|
|
||||||
>
|
|
||||||
{{ sectionResult.title }}
|
|
||||||
</h5>
|
|
||||||
<AppSpotlightEntry
|
|
||||||
v-for="(result, entryIndex) in sectionResult.results"
|
|
||||||
:key="`result-${result.id}`"
|
|
||||||
:entry="result"
|
|
||||||
:active="isEqual(selectedEntry, [sectionIndex, entryIndex])"
|
|
||||||
@mouseover="selectedEntry = [sectionIndex, entryIndex]"
|
|
||||||
@action="runAction(sectionID, result)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="search.length > 0 && scoredResults.length === 0"
|
|
||||||
:text="`${t('state.nothing_found')} ‟${search}”`"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
|
||||||
</template>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.clear')"
|
|
||||||
outline
|
|
||||||
@click="search = ''"
|
|
||||||
/>
|
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-shrink-0 text-tiny text-secondaryLight p-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<kbd class="shortcut-key">↑</kbd>
|
|
||||||
<kbd class="shortcut-key">↓</kbd>
|
|
||||||
<span class="mx-2 truncate">
|
|
||||||
{{ t("action.to_navigate") }}
|
|
||||||
</span>
|
|
||||||
<kbd class="shortcut-key">↩</kbd>
|
|
||||||
<span class="ml-2 truncate">
|
|
||||||
{{ t("action.to_select") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<kbd class="shortcut-key">ESC</kbd>
|
|
||||||
<span class="ml-2 truncate">
|
|
||||||
{{ t("action.to_close") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, watch } from "vue"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import {
|
|
||||||
SpotlightService,
|
|
||||||
SpotlightSearchState,
|
|
||||||
SpotlightSearcherResult,
|
|
||||||
} from "~/services/spotlight"
|
|
||||||
import { isEqual } from "lodash-es"
|
|
||||||
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
|
|
||||||
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
|
||||||
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
|
|
||||||
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
|
||||||
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
|
|
||||||
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
|
||||||
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
|
||||||
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
|
||||||
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
|
||||||
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
|
||||||
import {
|
|
||||||
EnvironmentsSpotlightSearcherService,
|
|
||||||
SwitchEnvSpotlightSearcherService,
|
|
||||||
} from "~/services/spotlight/searchers/environment.searcher"
|
|
||||||
import {
|
|
||||||
SwitchWorkspaceSpotlightSearcherService,
|
|
||||||
WorkspaceSpotlightSearcherService,
|
|
||||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
|
||||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
show: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const spotlightService = useService(SpotlightService)
|
|
||||||
|
|
||||||
useService(HistorySpotlightSearcherService)
|
|
||||||
useService(UserSpotlightSearcherService)
|
|
||||||
useService(NavigationSpotlightSearcherService)
|
|
||||||
useService(SettingsSpotlightSearcherService)
|
|
||||||
useService(CollectionsSpotlightSearcherService)
|
|
||||||
useService(MiscellaneousSpotlightSearcherService)
|
|
||||||
useService(TabSpotlightSearcherService)
|
|
||||||
useService(GeneralSpotlightSearcherService)
|
|
||||||
useService(ResponseSpotlightSearcherService)
|
|
||||||
useService(RequestSpotlightSearcherService)
|
|
||||||
useService(EnvironmentsSpotlightSearcherService)
|
|
||||||
useService(SwitchEnvSpotlightSearcherService)
|
|
||||||
useService(WorkspaceSpotlightSearcherService)
|
|
||||||
useService(SwitchWorkspaceSpotlightSearcherService)
|
|
||||||
useService(InterceptorSpotlightSearcherService)
|
|
||||||
|
|
||||||
const search = ref("")
|
|
||||||
|
|
||||||
const searchSession = ref<SpotlightSearchState>()
|
|
||||||
const stopSearchSession = ref<() => void>()
|
|
||||||
|
|
||||||
const scoredResults = computed(() =>
|
|
||||||
Object.entries(searchSession.value?.results ?? {}).sort(
|
|
||||||
([, sectionA], [, sectionB]) => sectionB.avgScore - sectionA.avgScore
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const { selectedEntry } = newUseArrowKeysForNavigation()
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.show,
|
|
||||||
(show) => {
|
|
||||||
search.value = ""
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
const [session, onSessionEnd] =
|
|
||||||
spotlightService.createSearchSession(search)
|
|
||||||
|
|
||||||
searchSession.value = session.value
|
|
||||||
stopSearchSession.value = onSessionEnd
|
|
||||||
} else {
|
|
||||||
stopSearchSession.value?.()
|
|
||||||
stopSearchSession.value = undefined
|
|
||||||
searchSession.value = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function runAction(searcherID: string, result: SpotlightSearcherResult) {
|
|
||||||
spotlightService.selectSearchResult(searcherID, result)
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
function newUseArrowKeysForNavigation() {
|
|
||||||
const selectedEntry = ref<[number, number]>([0, 0]) // [sectionIndex, entryIndex]
|
|
||||||
|
|
||||||
watch(search, () => {
|
|
||||||
selectedEntry.value = [0, 0]
|
|
||||||
})
|
|
||||||
|
|
||||||
const onArrowDown = () => {
|
|
||||||
// If no entries, do nothing
|
|
||||||
if (scoredResults.value.length === 0) return
|
|
||||||
|
|
||||||
const [sectionIndex, entryIndex] = selectedEntry.value
|
|
||||||
|
|
||||||
const [, section] = scoredResults.value[sectionIndex]
|
|
||||||
|
|
||||||
if (entryIndex < section.results.length - 1) {
|
|
||||||
selectedEntry.value = [sectionIndex, entryIndex + 1]
|
|
||||||
} else if (sectionIndex < scoredResults.value.length - 1) {
|
|
||||||
selectedEntry.value = [sectionIndex + 1, 0]
|
|
||||||
} else {
|
|
||||||
selectedEntry.value = [0, 0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onArrowUp = () => {
|
|
||||||
// If no entries, do nothing
|
|
||||||
if (scoredResults.value.length === 0) return
|
|
||||||
|
|
||||||
const [sectionIndex, entryIndex] = selectedEntry.value
|
|
||||||
|
|
||||||
if (entryIndex > 0) {
|
|
||||||
selectedEntry.value = [sectionIndex, entryIndex - 1]
|
|
||||||
} else if (sectionIndex > 0) {
|
|
||||||
const [, section] = scoredResults.value[sectionIndex - 1]
|
|
||||||
selectedEntry.value = [sectionIndex - 1, section.results.length - 1]
|
|
||||||
} else {
|
|
||||||
selectedEntry.value = [
|
|
||||||
scoredResults.value.length - 1,
|
|
||||||
scoredResults.value[scoredResults.value.length - 1][1].results.length -
|
|
||||||
1,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEnter = () => {
|
|
||||||
// If no entries, do nothing
|
|
||||||
if (scoredResults.value.length === 0) return
|
|
||||||
|
|
||||||
const [sectionIndex, entryIndex] = selectedEntry.value
|
|
||||||
const [sectionID, section] = scoredResults.value[sectionIndex]
|
|
||||||
const result = section.results[entryIndex]
|
|
||||||
|
|
||||||
runAction(sectionID, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyPress(e: KeyboardEvent) {
|
|
||||||
if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
onArrowUp()
|
|
||||||
} else if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
onArrowDown()
|
|
||||||
} else if (e.key === "Enter") {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
onEnter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.show,
|
|
||||||
(show) => {
|
|
||||||
if (show) {
|
|
||||||
window.addEventListener("keydown", handleKeyPress)
|
|
||||||
} else {
|
|
||||||
window.removeEventListener("keydown", handleKeyPress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return { selectedEntry }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelAdd"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="addNewCollection"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="addNewCollection"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelAdd">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -57,28 +65,28 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const editingName = ref("")
|
const name = ref("")
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (!show) {
|
if (!show) {
|
||||||
editingName.value = ""
|
name.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const addNewCollection = () => {
|
const addNewCollection = () => {
|
||||||
if (!editingName.value) {
|
if (!name.value) {
|
||||||
toast.error(t("collection.invalid_name"))
|
toast.error(t("collection.invalid_name"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("submit", editingName.value)
|
emit("submit", name.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = ""
|
name.value = ""
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="emit('hide-modal')"
|
@close="emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelAddFolder"
|
||||||
input-styles="floating-input"
|
v-model="name"
|
||||||
:label="t('action.label')"
|
v-focus
|
||||||
@submit="addFolder"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="addFolder"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelAddFolder">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -57,27 +65,27 @@ const emit = defineEmits<{
|
|||||||
(e: "add-folder", name: string): void
|
(e: "add-folder", name: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const editingName = ref("")
|
const name = ref("")
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (!show) {
|
if (!show) {
|
||||||
editingName.value = ""
|
name.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const addFolder = () => {
|
const addFolder = () => {
|
||||||
if (editingName.value.trim() === "") {
|
if (name.value.trim() === "") {
|
||||||
toast.error(t("folder.invalid_name"))
|
toast.error(t("folder.invalid_name"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emit("add-folder", editingName.value)
|
emit("add-folder", name.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = ""
|
name.value = ""
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,13 +6,19 @@
|
|||||||
@close="$emit('hide-modal')"
|
@close="$emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelAddRequest"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="addRequest"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="addRequest"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelAddRequest">{{ t("action.label") }}</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -58,23 +64,23 @@ const emit = defineEmits<{
|
|||||||
(event: "add-request", name: string): void
|
(event: "add-request", name: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const editingName = ref("")
|
const name = ref("")
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
editingName.value = currentActiveTab.value.document.request.name
|
name.value = currentActiveTab.value.document.request.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const addRequest = () => {
|
const addRequest = () => {
|
||||||
if (editingName.value.trim() === "") {
|
if (name.value.trim() === "") {
|
||||||
toast.error(`${t("error.empty_req_name")}`)
|
toast.error(`${t("error.empty_req_name")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emit("add-request", editingName.value)
|
emit("add-request", name.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
|||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
import IconFolder from "~icons/lucide/folder"
|
import IconFolder from "~icons/lucide/folder"
|
||||||
import IconFolderOpen from "~icons/lucide/folder-open"
|
import IconFolderOpen from "~icons/lucide/folder-open"
|
||||||
import { ref, computed, watch } from "vue"
|
import { PropType, ref, computed, watch } from "vue"
|
||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
@@ -209,36 +209,67 @@ type FolderType = "collection" | "folder"
|
|||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = defineProps({
|
||||||
defineProps<{
|
id: {
|
||||||
id: string
|
type: String,
|
||||||
parentID?: string | null
|
default: "",
|
||||||
data: HoppCollection<HoppRESTRequest> | TeamCollection
|
required: true,
|
||||||
/**
|
},
|
||||||
* Collection component can be used for both collections and folders.
|
parentID: {
|
||||||
* folderType is used to determine which one it is.
|
type: String as PropType<string | null>,
|
||||||
*/
|
default: null,
|
||||||
collectionsType: CollectionType
|
required: false,
|
||||||
folderType: FolderType
|
},
|
||||||
isOpen: boolean
|
data: {
|
||||||
isSelected?: boolean | null
|
type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
|
||||||
exportLoading?: boolean
|
default: () => ({}),
|
||||||
hasNoTeamAccess?: boolean
|
required: true,
|
||||||
collectionMoveLoading?: string[]
|
},
|
||||||
isLastItem?: boolean
|
collectionsType: {
|
||||||
}>(),
|
type: String as PropType<CollectionType>,
|
||||||
{
|
default: "my-collections",
|
||||||
id: "",
|
required: true,
|
||||||
parentID: null,
|
},
|
||||||
collectionsType: "my-collections",
|
/**
|
||||||
folderType: "collection",
|
* Collection component can be used for both collections and folders.
|
||||||
isOpen: false,
|
* folderType is used to determine which one it is.
|
||||||
isSelected: false,
|
*/
|
||||||
exportLoading: false,
|
folderType: {
|
||||||
hasNoTeamAccess: false,
|
type: String as PropType<FolderType>,
|
||||||
isLastItem: false,
|
default: "collection",
|
||||||
}
|
required: true,
|
||||||
)
|
},
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
type: Boolean as PropType<boolean | null>,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
exportLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
hasNoTeamAccess: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
collectionMoveLoading: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
isLastItem: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "toggle-children"): void
|
(event: "toggle-children"): void
|
||||||
@@ -417,13 +448,8 @@ const notSameDestination = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isCollLoading = computed(() => {
|
const isCollLoading = computed(() => {
|
||||||
const { collectionMoveLoading } = props
|
if (props.collectionMoveLoading.length > 0 && props.data.id) {
|
||||||
if (
|
return props.collectionMoveLoading.includes(props.data.id)
|
||||||
collectionMoveLoading &&
|
|
||||||
collectionMoveLoading.length > 0 &&
|
|
||||||
props.data.id
|
|
||||||
) {
|
|
||||||
return collectionMoveLoading.includes(props.data.id)
|
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelEdit"
|
||||||
input-styles="floating-input"
|
v-model="name"
|
||||||
:label="t('action.label')"
|
v-focus
|
||||||
@submit="saveCollection"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="saveCollection"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelEdit">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -59,26 +67,26 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const editingName = ref("")
|
const name = ref("")
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.editingCollectionName,
|
() => props.editingCollectionName,
|
||||||
(newName) => {
|
(newName) => {
|
||||||
editingName.value = newName
|
name.value = newName
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveCollection = () => {
|
const saveCollection = () => {
|
||||||
if (editingName.value.trim() === "") {
|
if (name.value.trim() === "") {
|
||||||
toast.error(t("collection.invalid_name"))
|
toast.error(t("collection.invalid_name"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("submit", editingName.value)
|
emit("submit", name.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = ""
|
name.value = ""
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="emit('hide-modal')"
|
@close="emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelEditFolder"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="editFolder"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="editFolder"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelEditFolder">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -59,26 +67,26 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const editingName = ref("")
|
const name = ref("")
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.editingFolderName,
|
() => props.editingFolderName,
|
||||||
(newName) => {
|
(newName) => {
|
||||||
editingName.value = newName
|
name.value = newName
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const editFolder = () => {
|
const editFolder = () => {
|
||||||
if (editingName.value.trim() === "") {
|
if (name.value.trim() === "") {
|
||||||
toast.error(t("folder.invalid_name"))
|
toast.error(t("folder.invalid_name"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("submit", editingName.value)
|
emit("submit", name.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = ""
|
name.value = ""
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelEditReq"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="editRequest"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="editRequest"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelEditReq">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -60,19 +68,19 @@ const emit = defineEmits<{
|
|||||||
(e: "update:modelValue", value: string): void
|
(e: "update:modelValue", value: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const editingName = useVModel(props, "modelValue")
|
const name = useVModel(props, "modelValue")
|
||||||
|
|
||||||
const editRequest = () => {
|
const editRequest = () => {
|
||||||
if (editingName.value.trim() === "") {
|
if (name.value.trim() === "") {
|
||||||
toast.error(t("request.invalid_name"))
|
toast.error(t("request.invalid_name"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("submit", editingName.value)
|
emit("submit", name.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = ""
|
name.value = ""
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<HoppSmartTree :adapter="myAdapter">
|
<SmartTree :adapter="myAdapter">
|
||||||
<template
|
<template
|
||||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||||
>
|
>
|
||||||
@@ -243,33 +243,49 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #emptyNode="{ node }">
|
<template #emptyNode="{ node }">
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="filterText.length !== 0 && filteredCollections.length === 0"
|
v-if="filterText.length !== 0 && filteredCollections.length === 0"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<span class="my-2 text-center">
|
||||||
</template>
|
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||||
</HoppSmartPlaceholder>
|
</span>
|
||||||
<HoppSmartPlaceholder
|
</div>
|
||||||
v-else-if="node === null"
|
<div v-else-if="node === null">
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
<div
|
||||||
:alt="`${t('empty.collections')}`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:text="t('empty.collections')"
|
>
|
||||||
>
|
<img
|
||||||
<HoppButtonSecondary
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:label="t('add.new')"
|
loading="lazy"
|
||||||
filled
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
outline
|
:alt="`${t('empty.collections')}`"
|
||||||
@click="emit('display-modal-add')"
|
/>
|
||||||
/>
|
<span class="pb-4 text-center">
|
||||||
</HoppSmartPlaceholder>
|
{{ t("empty.collections") }}
|
||||||
<HoppSmartPlaceholder
|
</span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('add.new')"
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.collections')}`"
|
|
||||||
:text="t('empty.collections')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
|
:alt="`${t('empty.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collection") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:label="t('add.new')"
|
||||||
filled
|
filled
|
||||||
@@ -282,16 +298,23 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.folder')}`"
|
|
||||||
:text="t('empty.folder')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
|
:alt="`${t('empty.folder')}`"
|
||||||
|
/>
|
||||||
|
<span class="text-center">
|
||||||
|
{{ t("empty.folder") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartTree>
|
</SmartTree>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -303,10 +326,7 @@ import IconHelpCircle from "~icons/lucide/help-circle"
|
|||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { computed, PropType, Ref, toRef } from "vue"
|
import { computed, PropType, Ref, toRef } from "vue"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import {
|
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||||
ChildrenResult,
|
|
||||||
SmartTreeAdapter,
|
|
||||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
|
|||||||
@@ -8,15 +8,21 @@
|
|||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<HoppSmartInput
|
<div class="relative flex">
|
||||||
v-model="requestName"
|
<input
|
||||||
styles="relative flex"
|
id="selectLabelSaveReq"
|
||||||
placeholder=" "
|
v-model="requestName"
|
||||||
:label="t('request.name')"
|
v-focus
|
||||||
input-styles="floating-input"
|
class="input floating-input"
|
||||||
@submit="saveRequestAs"
|
placeholder=" "
|
||||||
/>
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="saveRequestAs"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelSaveReq">
|
||||||
|
{{ t("request.name") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<label class="p-4">
|
<label class="p-4">
|
||||||
{{ t("collection.select_location") }}
|
{{ t("collection.select_location") }}
|
||||||
</label>
|
</label>
|
||||||
@@ -56,7 +62,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, reactive, ref, watch } from "vue"
|
import { nextTick, reactive, ref, watch } from "vue"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import {
|
import {
|
||||||
HoppGQLRequest,
|
HoppGQLRequest,
|
||||||
@@ -71,6 +77,7 @@ import {
|
|||||||
updateTeamRequest,
|
updateTeamRequest,
|
||||||
} from "~/helpers/backend/mutations/TeamRequest"
|
} from "~/helpers/backend/mutations/TeamRequest"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
|
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import {
|
import {
|
||||||
@@ -81,9 +88,8 @@ import {
|
|||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
import { computedWithControl } from "@vueuse/core"
|
import { computedWithControl } from "@vueuse/core"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
|
|
||||||
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -101,12 +107,10 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
mode: "rest" | "graphql"
|
mode: "rest" | "graphql"
|
||||||
request?: HoppRESTRequest | HoppGQLRequest | null
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
show: false,
|
show: false,
|
||||||
mode: "rest",
|
mode: "rest",
|
||||||
request: null,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,36 +126,22 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const gqlRequestName = computedWithControl(
|
const gqlRequestName = useGQLRequestName()
|
||||||
() => activeGQLTab.value,
|
|
||||||
() => activeGQLTab.value.document.request.name
|
|
||||||
)
|
|
||||||
|
|
||||||
const restRequestName = computedWithControl(
|
const restRequestName = computedWithControl(
|
||||||
() => activeRESTTab.value,
|
() => currentActiveTab.value,
|
||||||
() => activeRESTTab.value.document.request.name
|
() => currentActiveTab.value.document.request.name
|
||||||
)
|
)
|
||||||
|
|
||||||
const reqName = computed(() => {
|
const requestName = ref(
|
||||||
if (props.request) {
|
props.mode === "rest" ? restRequestName.value : gqlRequestName.value
|
||||||
return props.request.name
|
)
|
||||||
} else if (props.mode === "rest") {
|
|
||||||
return restRequestName.value
|
|
||||||
} else {
|
|
||||||
return gqlRequestName.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const requestName = ref(reqName.value)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [activeRESTTab.value, activeGQLTab.value],
|
() => [currentActiveTab.value, gqlRequestName.value],
|
||||||
() => {
|
() => {
|
||||||
if (props.mode === "rest") {
|
if (props.mode === "rest") {
|
||||||
requestName.value = activeRESTTab.value?.document.request.name ?? ""
|
requestName.value = currentActiveTab.value?.document.request.name ?? ""
|
||||||
} else {
|
} else requestName.value = gqlRequestName.value
|
||||||
requestName.value = activeGQLTab.value?.document.request.name ?? ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -210,8 +200,8 @@ const saveRequestAs = async () => {
|
|||||||
|
|
||||||
const requestUpdated =
|
const requestUpdated =
|
||||||
props.mode === "rest"
|
props.mode === "rest"
|
||||||
? cloneDeep(activeRESTTab.value.document.request)
|
? cloneDeep(currentActiveTab.value.document.request)
|
||||||
: cloneDeep(activeGQLTab.value.document.request)
|
: cloneDeep(getGQLSession().request)
|
||||||
|
|
||||||
requestUpdated.name = requestName.value
|
requestUpdated.name = requestName.value
|
||||||
|
|
||||||
@@ -224,7 +214,7 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -251,7 +241,7 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -279,7 +269,7 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -439,7 +429,7 @@ const updateTeamCollectionOrFolder = (
|
|||||||
(result) => {
|
(result) => {
|
||||||
const { createRequestInCollection } = result
|
const { createRequestInCollection } = result
|
||||||
|
|
||||||
activeRESTTab.value.document = {
|
currentActiveTab.value.document = {
|
||||||
request: requestUpdated,
|
request: requestUpdated,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
@@ -460,7 +450,7 @@ const updateTeamCollectionOrFolder = (
|
|||||||
const requestSaved = () => {
|
const requestSaved = () => {
|
||||||
toast.success(`${t("request.added")}`)
|
toast.success(`${t("request.added")}`)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
activeRESTTab.value.document.isDirty = false
|
currentActiveTab.value.document.isDirty = false
|
||||||
})
|
})
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<HoppSmartTree :adapter="teamAdapter">
|
<SmartTree :adapter="teamAdapter">
|
||||||
<template
|
<template
|
||||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||||
>
|
>
|
||||||
@@ -262,56 +262,70 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #emptyNode="{ node }">
|
<template #emptyNode="{ node }">
|
||||||
<div v-if="node === null">
|
<div v-if="node === null">
|
||||||
<div @drop="(e) => e.stopPropagation()">
|
<div
|
||||||
<HoppSmartPlaceholder
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
@drop="(e) => e.stopPropagation()"
|
||||||
|
>
|
||||||
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:alt="`${t('empty.collections')}`"
|
loading="lazy"
|
||||||
:text="t('empty.collections')"
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
>
|
:alt="`${t('empty.collection')}`"
|
||||||
<HoppButtonSecondary
|
/>
|
||||||
v-if="hasNoTeamAccess"
|
<span class="pb-4 text-center">
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
{{ t("empty.collections") }}
|
||||||
disabled
|
</span>
|
||||||
filled
|
<HoppButtonSecondary
|
||||||
outline
|
v-if="hasNoTeamAccess"
|
||||||
:title="t('team.no_access')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:label="t('action.new')"
|
disabled
|
||||||
/>
|
filled
|
||||||
<HoppButtonSecondary
|
outline
|
||||||
v-else
|
:title="t('team.no_access')"
|
||||||
:icon="IconPlus"
|
:label="t('action.new')"
|
||||||
:label="t('action.new')"
|
/>
|
||||||
filled
|
<HoppButtonSecondary
|
||||||
outline
|
v-else
|
||||||
@click="emit('display-modal-add')"
|
:icon="IconPlus"
|
||||||
/>
|
:label="t('action.new')"
|
||||||
</HoppSmartPlaceholder>
|
filled
|
||||||
|
outline
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
@drop="(e) => e.stopPropagation()"
|
@drop="(e) => e.stopPropagation()"
|
||||||
>
|
>
|
||||||
<HoppSmartPlaceholder
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:alt="`${t('empty.collections')}`"
|
loading="lazy"
|
||||||
:text="t('empty.collections')"
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
>
|
:alt="`${t('empty.collection')}`"
|
||||||
</HoppSmartPlaceholder>
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collections") }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
@drop="(e) => e.stopPropagation()"
|
@drop="(e) => e.stopPropagation()"
|
||||||
>
|
>
|
||||||
<HoppSmartPlaceholder
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
:alt="`${t('empty.folder')}`"
|
:alt="`${t('empty.folder')}`"
|
||||||
:text="t('empty.folder')"
|
/>
|
||||||
>
|
<span class="text-center">
|
||||||
</HoppSmartPlaceholder>
|
{{ t("empty.folder") }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartTree>
|
</SmartTree>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -326,10 +340,7 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||||
import { TeamRequest } from "~/helpers/teams/TeamRequest"
|
import { TeamRequest } from "~/helpers/teams/TeamRequest"
|
||||||
import {
|
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||||
ChildrenResult,
|
|
||||||
SmartTreeAdapter,
|
|
||||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="name"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelGqlAdd"
|
||||||
input-styles="floating-input"
|
v-model="name"
|
||||||
:label="t('action.label')"
|
v-focus
|
||||||
@submit="addNewCollection"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="addNewCollection"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelGqlAdd">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="$emit('hide-modal')"
|
@close="$emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="name"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelGqlAddFolder"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="addFolder"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="addFolder"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelGqlAddFolder">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="emit('hide-modal')"
|
@close="emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelGqlAddRequest"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="addRequest"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="addRequest"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelGqlAddRequest">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -36,7 +44,7 @@
|
|||||||
import { ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
import { getGQLSession } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -57,24 +65,24 @@ const emit = defineEmits<{
|
|||||||
): void
|
): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const editingName = ref("")
|
const name = ref("")
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
editingName.value = currentActiveTab.value?.document.request.name
|
name.value = getGQLSession().request.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const addRequest = () => {
|
const addRequest = () => {
|
||||||
if (!editingName.value) {
|
if (!name.value) {
|
||||||
toast.error(`${t("error.empty_req_name")}`)
|
toast.error(`${t("error.empty_req_name")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emit("add-request", {
|
emit("add-request", {
|
||||||
name: editingName.value,
|
name: name.value,
|
||||||
path: props.folderPath,
|
path: props.folderPath,
|
||||||
})
|
})
|
||||||
hideModal()
|
hideModal()
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
@click="
|
@click="
|
||||||
emit('add-request', {
|
emit('add-request', {
|
||||||
path: `${collectionIndex}`,
|
path: `${collectionIndex}`,
|
||||||
index: collection.requests.length,
|
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -172,14 +171,21 @@
|
|||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
collection.folders.length === 0 && collection.requests.length === 0
|
collection.folders.length === 0 && collection.requests.length === 0
|
||||||
"
|
"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.collection')}`"
|
|
||||||
:text="t('empty.collection')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
|
:alt="`${t('empty.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collection") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:label="t('add.new')"
|
||||||
filled
|
filled
|
||||||
@@ -190,7 +196,7 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
@@ -220,7 +226,6 @@ import {
|
|||||||
moveGraphqlRequest,
|
moveGraphqlRequest,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
picked: { type: Object, default: null },
|
picked: { type: Object, default: null },
|
||||||
@@ -295,22 +300,6 @@ const removeCollection = () => {
|
|||||||
emit("select", null)
|
emit("select", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleTabs = getTabsRefTo((tab) => {
|
|
||||||
const ctx = tab.document.saveContext
|
|
||||||
|
|
||||||
if (!ctx) return false
|
|
||||||
|
|
||||||
return (
|
|
||||||
ctx.originLocation === "user-collection" &&
|
|
||||||
ctx.folderPath.startsWith(props.collectionIndex.toString())
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const tab of possibleTabs) {
|
|
||||||
tab.value.document.saveContext = undefined
|
|
||||||
tab.value.document.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGraphqlCollection(props.collectionIndex, props.collection.id)
|
removeGraphqlCollection(props.collectionIndex, props.collection.id)
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="editingName"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelGqlEdit"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="saveCollection"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="saveCollection"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelGqlEdit">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
@@ -52,17 +60,17 @@ const emit = defineEmits<{
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const editingName = ref<string | null>()
|
const name = ref<string | null>()
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.editingCollectionName,
|
() => props.editingCollectionName,
|
||||||
(val) => {
|
(val) => {
|
||||||
editingName.value = val
|
name.value = val
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveCollection = () => {
|
const saveCollection = () => {
|
||||||
if (!editingName.value) {
|
if (!name.value) {
|
||||||
toast.error(`${t("collection.invalid_name")}`)
|
toast.error(`${t("collection.invalid_name")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,7 +78,7 @@ const saveCollection = () => {
|
|||||||
// TODO: Better typechecking here ?
|
// TODO: Better typechecking here ?
|
||||||
const collectionUpdated = {
|
const collectionUpdated = {
|
||||||
...(props.editingCollection as any),
|
...(props.editingCollection as any),
|
||||||
name: editingName.value,
|
name: name.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
editGraphqlCollection(props.editingCollectionIndex, collectionUpdated)
|
editGraphqlCollection(props.editingCollectionIndex, collectionUpdated)
|
||||||
@@ -78,7 +86,7 @@ const saveCollection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
editingName.value = null
|
name.value = null
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="$emit('hide-modal')"
|
@close="$emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="name"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelGqlEditFolder"
|
||||||
:label="t('action.label')"
|
v-model="name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="editFolder"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="editFolder"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelGqlEditFolder">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
|
|||||||
@@ -6,13 +6,21 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<HoppSmartInput
|
<div class="flex flex-col">
|
||||||
v-model="requestUpdateData.name"
|
<input
|
||||||
placeholder=" "
|
id="selectLabelGqlEditReq"
|
||||||
:label="t('action.label')"
|
v-model="requestUpdateData.name"
|
||||||
input-styles="floating-input"
|
v-focus
|
||||||
@submit="saveRequest"
|
class="input floating-input"
|
||||||
/>
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="saveRequest"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelGqlEditReq">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="flex space-x-2">
|
<span class="flex space-x-2">
|
||||||
|
|||||||
@@ -34,12 +34,7 @@
|
|||||||
:icon="IconFilePlus"
|
:icon="IconFilePlus"
|
||||||
:title="t('request.new')"
|
:title="t('request.new')"
|
||||||
class="hidden group-hover:inline-flex"
|
class="hidden group-hover:inline-flex"
|
||||||
@click="
|
@click="emit('add-request', { path: folderPath })"
|
||||||
emit('add-request', {
|
|
||||||
path: folderPath,
|
|
||||||
index: folder.requests.length,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -165,19 +160,25 @@
|
|||||||
@duplicate-request="emit('duplicate-request', $event)"
|
@duplicate-request="emit('duplicate-request', $event)"
|
||||||
@select="emit('select', $event)"
|
@select="emit('select', $event)"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="
|
v-if="
|
||||||
folder.folders &&
|
folder.folders &&
|
||||||
folder.folders.length === 0 &&
|
folder.folders.length === 0 &&
|
||||||
folder.requests &&
|
folder.requests &&
|
||||||
folder.requests.length === 0
|
folder.requests.length === 0
|
||||||
"
|
"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.folder')}`"
|
|
||||||
:text="t('empty.folder')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
|
:alt="`${t('empty.folder')}`"
|
||||||
|
/>
|
||||||
|
<span class="text-center">
|
||||||
|
{{ t("empty.folder") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
@@ -203,7 +204,6 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -255,8 +255,10 @@ const collectionIcon = computed(() => {
|
|||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
emit("select", {
|
emit("select", {
|
||||||
pickedType: "gql-my-folder",
|
picked: {
|
||||||
folderPath: props.folderPath,
|
pickedType: "gql-my-folder",
|
||||||
|
folderPath: props.folderPath,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,22 +279,6 @@ const removeFolder = () => {
|
|||||||
emit("select", { picked: null })
|
emit("select", { picked: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleTabs = getTabsRefTo((tab) => {
|
|
||||||
const ctx = tab.document.saveContext
|
|
||||||
|
|
||||||
if (!ctx) return false
|
|
||||||
|
|
||||||
return (
|
|
||||||
ctx.originLocation === "user-collection" &&
|
|
||||||
ctx.folderPath.startsWith(props.folderPath)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const tab of possibleTabs) {
|
|
||||||
tab.value.document.saveContext = undefined
|
|
||||||
tab.value.document.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGraphqlFolder(props.folderPath, props.folder.id)
|
removeGraphqlFolder(props.folderPath, props.folder.id)
|
||||||
toast.success(t("state.deleted"))
|
toast.success(t("state.deleted"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,28 +20,22 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||||
@click="selectRequest()"
|
@click="selectRequest()"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
{{ request.name }}
|
{{ request.name }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
v-if="isActive"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
|
||||||
:title="`${t('collection.request_in_use')}`"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="!saveRequest"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconRotateCCW"
|
||||||
|
:title="t('action.restore')"
|
||||||
|
class="hidden group-hover:inline-flex"
|
||||||
|
@click="selectRequest()"
|
||||||
|
/>
|
||||||
<span>
|
<span>
|
||||||
<tippy
|
<tippy
|
||||||
ref="options"
|
ref="options"
|
||||||
@@ -127,6 +121,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
import IconFile from "~icons/lucide/file"
|
import IconFile from "~icons/lucide/file"
|
||||||
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
@@ -137,12 +132,7 @@ import { useToast } from "@composables/toast"
|
|||||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||||
import {
|
import { setGQLSession } from "~/newstore/GQLSession"
|
||||||
createNewTab,
|
|
||||||
getTabRefWithSaveContext,
|
|
||||||
currentTabID,
|
|
||||||
currentActiveTab,
|
|
||||||
} from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
@@ -164,18 +154,6 @@ const props = defineProps({
|
|||||||
requestIndex: { type: Number, default: null },
|
requestIndex: { type: Number, default: null },
|
||||||
})
|
})
|
||||||
|
|
||||||
const isActive = computed(() => {
|
|
||||||
const saveCtx = currentActiveTab.value?.document.saveContext
|
|
||||||
|
|
||||||
if (!saveCtx) return false
|
|
||||||
|
|
||||||
return (
|
|
||||||
saveCtx.originLocation === "user-collection" &&
|
|
||||||
saveCtx.folderPath === props.folderPath &&
|
|
||||||
saveCtx.requestIndex === props.requestIndex
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Better types please
|
// TODO: Better types please
|
||||||
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
||||||
|
|
||||||
@@ -201,24 +179,7 @@ const selectRequest = () => {
|
|||||||
if (props.saveRequest) {
|
if (props.saveRequest) {
|
||||||
pick()
|
pick()
|
||||||
} else {
|
} else {
|
||||||
const possibleTab = getTabRefWithSaveContext({
|
setGQLSession({
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Switch to that request if that request is open
|
|
||||||
if (possibleTab) {
|
|
||||||
currentTabID.value = possibleTab.value.id
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewTab({
|
|
||||||
saveContext: {
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
},
|
|
||||||
request: cloneDeep(
|
request: cloneDeep(
|
||||||
makeGQLRequest({
|
makeGQLRequest({
|
||||||
name: props.request.name,
|
name: props.request.name,
|
||||||
@@ -229,7 +190,8 @@ const selectRequest = () => {
|
|||||||
auth: props.request.auth,
|
auth: props.request.auth,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
isDirty: false,
|
schema: "",
|
||||||
|
response: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,18 +214,6 @@ const removeRequest = () => {
|
|||||||
emit("select", null)
|
emit("select", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detach the request from any of the tabs
|
|
||||||
const possibleTab = getTabRefWithSaveContext({
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: props.folderPath,
|
|
||||||
requestIndex: props.requestIndex,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (possibleTab) {
|
|
||||||
possibleTab.value.document.saveContext = undefined
|
|
||||||
possibleTab.value.document.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
|
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:placeholder="t('action.search')"
|
:placeholder="t('action.search')"
|
||||||
class="py-2 pl-4 pr-2 bg-transparent !border-0"
|
class="py-2 pl-4 pr-2 bg-transparent"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
|
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
|
||||||
@@ -60,27 +60,35 @@
|
|||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="collections.length === 0"
|
v-if="collections.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.collections')}`"
|
|
||||||
:text="t('empty.collections')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="t('empty.collections')"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collections") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:label="t('add.new')"
|
||||||
filled
|
filled
|
||||||
outline
|
outline
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<span class="my-2 text-center">
|
||||||
</template>
|
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||||
</HoppSmartPlaceholder>
|
</span>
|
||||||
|
</div>
|
||||||
<CollectionsGraphqlAdd
|
<CollectionsGraphqlAdd
|
||||||
:show="showModalAdd"
|
:show="showModalAdd"
|
||||||
@hide-modal="displayModalAdd(false)"
|
@hide-modal="displayModalAdd(false)"
|
||||||
@@ -137,6 +145,7 @@ import {
|
|||||||
addGraphqlFolder,
|
addGraphqlFolder,
|
||||||
saveGraphqlRequestAs,
|
saveGraphqlRequestAs,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
|
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
@@ -145,7 +154,6 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -265,22 +273,17 @@ export default defineComponent({
|
|||||||
this.$data.editingCollectionIndex = collectionIndex
|
this.$data.editingCollectionIndex = collectionIndex
|
||||||
this.displayModalEdit(true)
|
this.displayModalEdit(true)
|
||||||
},
|
},
|
||||||
onAddRequest({ name, path, index }) {
|
onAddRequest({ name, path }) {
|
||||||
const newRequest = {
|
const newRequest = {
|
||||||
...currentActiveTab.value.document.request,
|
...getGQLSession().request,
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
|
|
||||||
saveGraphqlRequestAs(path, newRequest)
|
saveGraphqlRequestAs(path, newRequest)
|
||||||
|
setGQLSession({
|
||||||
createNewTab({
|
|
||||||
saveContext: {
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: path,
|
|
||||||
requestIndex: index,
|
|
||||||
},
|
|
||||||
request: newRequest,
|
request: newRequest,
|
||||||
isDirty: false,
|
schema: "",
|
||||||
|
response: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
|
|||||||
@@ -18,13 +18,12 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<WorkspaceCurrent :section="t('tab.collections')" />
|
<WorkspaceCurrent :section="t('tab.collections')" />
|
||||||
|
<input
|
||||||
<HoppSmartInput
|
|
||||||
v-model="filterTexts"
|
v-model="filterTexts"
|
||||||
:placeholder="t('action.search')"
|
|
||||||
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
|
|
||||||
type="search"
|
type="search"
|
||||||
:autofocus="false"
|
autocomplete="off"
|
||||||
|
:placeholder="t('action.search')"
|
||||||
|
class="py-2 pl-4 pr-2 bg-transparent"
|
||||||
:disabled="collectionsType.type === 'team-collections'"
|
:disabled="collectionsType.type === 'team-collections'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +238,6 @@ import {
|
|||||||
resetTeamRequestsContext,
|
resetTeamRequestsContext,
|
||||||
} from "~/helpers/collection/collection"
|
} from "~/helpers/collection/collection"
|
||||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -2068,8 +2066,4 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("collection.new", () => {
|
|
||||||
displayModalAdd(true)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
<template>
|
|
||||||
<HoppSmartModal
|
|
||||||
v-if="show"
|
|
||||||
:title="t('environment.set_as_environment')"
|
|
||||||
@close="hideModal"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<div class="flex space-y-4 flex-1 flex-col">
|
|
||||||
<div class="flex items-center space-x-8 ml-2">
|
|
||||||
<label for="name" class="font-semibold min-w-10">{{
|
|
||||||
t("environment.name")
|
|
||||||
}}</label>
|
|
||||||
<input
|
|
||||||
v-model="editingName"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('environment.variable')"
|
|
||||||
class="input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-8 ml-2">
|
|
||||||
<label for="value" class="font-semibold min-w-10">{{
|
|
||||||
t("environment.value")
|
|
||||||
}}</label>
|
|
||||||
<input type="text" :value="value" class="input" />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-8 ml-2">
|
|
||||||
<label for="scope" class="font-semibold min-w-10">
|
|
||||||
{{ t("environment.scope") }}
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="relative flex flex-1 flex-col border border-divider rounded focus-visible:border-dividerDark"
|
|
||||||
>
|
|
||||||
<EnvironmentsSelector v-model="scope" :is-scope-selector="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="replaceWithVariable" class="flex space-x-2 mt-3">
|
|
||||||
<div class="min-w-18" />
|
|
||||||
<HoppSmartCheckbox
|
|
||||||
:on="replaceWithVariable"
|
|
||||||
title="t('environment.replace_with_variable'))"
|
|
||||||
@change="replaceWithVariable = !replaceWithVariable"
|
|
||||||
/>
|
|
||||||
<label for="replaceWithVariable">
|
|
||||||
{{ t("environment.replace_with_variable") }}</label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<span class="flex space-x-2">
|
|
||||||
<HoppButtonPrimary
|
|
||||||
:label="t('action.save')"
|
|
||||||
outline
|
|
||||||
@click="addEnvironment"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.cancel')"
|
|
||||||
outline
|
|
||||||
filled
|
|
||||||
@click="hideModal"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Environment } from "@hoppscotch/data"
|
|
||||||
import { ref, watch } from "vue"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
|
||||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
|
||||||
import {
|
|
||||||
addEnvironmentVariable,
|
|
||||||
addGlobalEnvVariable,
|
|
||||||
} from "~/newstore/environments"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
|
||||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
show: boolean
|
|
||||||
position: { top: number; left: number }
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.show,
|
|
||||||
(newVal) => {
|
|
||||||
if (!newVal) {
|
|
||||||
scope.value = {
|
|
||||||
type: "global",
|
|
||||||
}
|
|
||||||
editingName.value = ""
|
|
||||||
replaceWithVariable.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type Scope =
|
|
||||||
| {
|
|
||||||
type: "global"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "my-environment"
|
|
||||||
environment: Environment
|
|
||||||
index: number
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "team-environment"
|
|
||||||
environment: TeamEnvironment
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = ref<Scope>({
|
|
||||||
type: "global",
|
|
||||||
})
|
|
||||||
|
|
||||||
const replaceWithVariable = ref(false)
|
|
||||||
|
|
||||||
const editingName = ref(props.name)
|
|
||||||
|
|
||||||
const addEnvironment = async () => {
|
|
||||||
if (!editingName.value) {
|
|
||||||
toast.error(`${t("environment.invalid_name")}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (scope.value.type === "global") {
|
|
||||||
addGlobalEnvVariable({
|
|
||||||
key: editingName.value,
|
|
||||||
value: props.value,
|
|
||||||
})
|
|
||||||
toast.success(`${t("environment.updated")}`)
|
|
||||||
} else if (scope.value.type === "my-environment") {
|
|
||||||
addEnvironmentVariable(scope.value.index, {
|
|
||||||
key: editingName.value,
|
|
||||||
value: props.value,
|
|
||||||
})
|
|
||||||
toast.success(`${t("environment.updated")}`)
|
|
||||||
} else {
|
|
||||||
const newVariables = [
|
|
||||||
...scope.value.environment.environment.variables,
|
|
||||||
{
|
|
||||||
key: editingName.value,
|
|
||||||
value: props.value,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
await pipe(
|
|
||||||
updateTeamEnvironment(
|
|
||||||
JSON.stringify(newVariables),
|
|
||||||
scope.value.environment.id,
|
|
||||||
scope.value.environment.environment.name
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
hideModal()
|
|
||||||
toast.success(`${t("environment.updated")}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
|
||||||
if (replaceWithVariable.value) {
|
|
||||||
//replace the current tab endpoint with the variable name with << and >>
|
|
||||||
const variableName = `<<${editingName.value}>>`
|
|
||||||
//replace the currenttab endpoint containing the value in the text with variablename
|
|
||||||
currentActiveTab.value.document.request.endpoint =
|
|
||||||
currentActiveTab.value.document.request.endpoint.replace(
|
|
||||||
props.value,
|
|
||||||
variableName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getErrorMessage = (err: GQLError<string>) => {
|
|
||||||
if (err.type === "network_error") {
|
|
||||||
return t("error.network_error")
|
|
||||||
} else {
|
|
||||||
switch (err.error) {
|
|
||||||
case "team_environment/not_found":
|
|
||||||
return t("team_environment.not_found")
|
|
||||||
case "Forbidden resource":
|
|
||||||
return t("profile.no_permission")
|
|
||||||
default:
|
|
||||||
return t("error.something_went_wrong")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user