Compare commits

..

1 Commits

Author SHA1 Message Date
Liyas Thomas
1dcfc684ef feat: revamped header wireframe 2023-12-01 19:49:23 +05:30
343 changed files with 9048 additions and 11747 deletions

View File

@@ -66,7 +66,6 @@ services:
# The service that spins up all 3 services at once in one container # The service that spins up all 3 services at once in one container
hoppscotch-aio: hoppscotch-aio:
container_name: hoppscotch-aio container_name: hoppscotch-aio
restart: unless-stopped
build: build:
dockerfile: prod.Dockerfile dockerfile: prod.Dockerfile
context: . context: .

View File

@@ -32,9 +32,6 @@
"lint-staged": "12.4.0" "lint-staged": "12.4.0"
}, },
"pnpm": { "pnpm": {
"overrides": {
"vue": "3.3.9"
},
"packageExtensions": { "packageExtensions": {
"httpsnippet@^3.0.1": { "httpsnippet@^3.0.1": {
"peerDependencies": { "peerDependencies": {

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

@@ -28,7 +28,6 @@
"@nestjs-modules/mailer": "^1.9.1", "@nestjs-modules/mailer": "^1.9.1",
"@nestjs/apollo": "^12.0.9", "@nestjs/apollo": "^12.0.9",
"@nestjs/common": "^10.2.6", "@nestjs/common": "^10.2.6",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.6", "@nestjs/core": "^10.2.6",
"@nestjs/graphql": "^12.0.9", "@nestjs/graphql": "^12.0.9",
"@nestjs/jwt": "^10.1.1", "@nestjs/jwt": "^10.1.1",

View File

@@ -1,14 +0,0 @@
-- CreateTable
CREATE TABLE "InfraConfig" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InfraConfig_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "InfraConfig_name_key" ON "InfraConfig"("name");

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB;
-- AlterTable
ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB;

View File

@@ -43,7 +43,6 @@ model TeamInvitation {
model TeamCollection { model TeamCollection {
id String @id @default(cuid()) id String @id @default(cuid())
parentID String? parentID String?
data Json?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id]) parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent") children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[] requests TeamRequest[]
@@ -75,8 +74,7 @@ model Shortcode {
creatorUid String? creatorUid String?
User User? @relation(fields: [creatorUid], references: [uid]) User User? @relation(fields: [creatorUid], references: [uid])
createdOn DateTime @default(now()) createdOn DateTime @default(now())
updatedOn DateTime @default(now()) @updatedAt updatedOn DateTime @updatedAt @default(now())
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique") @@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
} }
@@ -197,7 +195,6 @@ model UserCollection {
userUid String userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade) user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
title String title String
data Json?
orderIndex Int orderIndex Int
type ReqType type ReqType
createdOn DateTime @default(now()) @db.Timestamp(3) createdOn DateTime @default(now()) @db.Timestamp(3)
@@ -209,12 +206,3 @@ enum TeamMemberRole {
VIEWER VIEWER
EDITOR EDITOR
} }
model InfraConfig {
id String @id @default(cuid())
name String @unique
value String?
active Boolean @default(true) // Use case: Let's say, Admin wants to disable Google SSO, but doesn't want to delete the config
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}

View File

@@ -4,6 +4,7 @@ import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module'; import { PrismaModule } from '../prisma/prisma.module';
import { PubSubModule } from '../pubsub/pubsub.module'; import { PubSubModule } from '../pubsub/pubsub.module';
import { UserModule } from '../user/user.module'; import { UserModule } from '../user/user.module';
import { MailerModule } from '../mailer/mailer.module';
import { TeamModule } from '../team/team.module'; import { TeamModule } from '../team/team.module';
import { TeamInvitationModule } from '../team-invitation/team-invitation.module'; import { TeamInvitationModule } from '../team-invitation/team-invitation.module';
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module'; import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
@@ -11,20 +12,19 @@ import { TeamCollectionModule } from '../team-collection/team-collection.module'
import { TeamRequestModule } from '../team-request/team-request.module'; import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver'; import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module'; import { ShortcodeModule } from 'src/shortcode/shortcode.module';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({ @Module({
imports: [ imports: [
PrismaModule, PrismaModule,
PubSubModule, PubSubModule,
UserModule, UserModule,
MailerModule,
TeamModule, TeamModule,
TeamInvitationModule, TeamInvitationModule,
TeamEnvironmentsModule, TeamEnvironmentsModule,
TeamCollectionModule, TeamCollectionModule,
TeamRequestModule, TeamRequestModule,
ShortcodeModule, ShortcodeModule,
InfraConfigModule,
], ],
providers: [InfraResolver, AdminResolver, AdminService], providers: [InfraResolver, AdminResolver, AdminService],
exports: [AdminService], exports: [AdminService],

View File

@@ -16,7 +16,6 @@ import {
USER_ALREADY_INVITED, USER_ALREADY_INVITED,
} from '../errors'; } from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service'; import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>(); const mockPubSub = mockDeep<PubSubService>();
@@ -28,7 +27,6 @@ const mockTeamInvitationService = mockDeep<TeamInvitationService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>(); const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockMailerService = mockDeep<MailerService>(); const mockMailerService = mockDeep<MailerService>();
const mockShortcodeService = mockDeep<ShortcodeService>(); const mockShortcodeService = mockDeep<ShortcodeService>();
const mockConfigService = mockDeep<ConfigService>();
const adminService = new AdminService( const adminService = new AdminService(
mockUserService, mockUserService,
@@ -41,7 +39,6 @@ const adminService = new AdminService(
mockPrisma as any, mockPrisma as any,
mockMailerService, mockMailerService,
mockShortcodeService, mockShortcodeService,
mockConfigService,
); );
const invitedUsers: InvitedUsers[] = [ const invitedUsers: InvitedUsers[] = [

View File

@@ -25,7 +25,6 @@ import { TeamEnvironmentsService } from '../team-environments/team-environments.
import { TeamInvitationService } from '../team-invitation/team-invitation.service'; import { TeamInvitationService } from '../team-invitation/team-invitation.service';
import { TeamMemberRole } from '../team/team.model'; import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service'; import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
@@ -40,7 +39,6 @@ export class AdminService {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService, private readonly shortcodeService: ShortcodeService,
private readonly configService: ConfigService,
) {} ) {}
/** /**
@@ -81,7 +79,7 @@ export class AdminService {
template: 'user-invitation', template: 'user-invitation',
variables: { variables: {
inviteeEmail: inviteeEmail, inviteeEmail: inviteeEmail,
magicLink: `${this.configService.get('VITE_BASE_URL')}`, magicLink: `${process.env.VITE_BASE_URL}`,
}, },
}); });
} catch (e) { } catch (e) {

View File

@@ -1,12 +1,5 @@
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
import { import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
Args,
ID,
Mutation,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { Infra } from './infra.model'; import { Infra } from './infra.model';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
@@ -23,21 +16,11 @@ import { Team } from 'src/team/team.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { GqlAdmin } from './decorators/gql-admin.decorator'; import { GqlAdmin } from './decorators/gql-admin.decorator';
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model'; import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
import { InfraConfig } from 'src/infra-config/infra-config.model';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
import {
EnableAndDisableSSOArgs,
InfraConfigArgs,
} from 'src/infra-config/input-args';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@Resolver(() => Infra) @Resolver(() => Infra)
export class InfraResolver { export class InfraResolver {
constructor( constructor(private adminService: AdminService) {}
private adminService: AdminService,
private infraConfigService: InfraConfigService,
) {}
@Query(() => Infra, { @Query(() => Infra, {
description: 'Fetch details of the Infrastructure', description: 'Fetch details of the Infrastructure',
@@ -239,76 +222,4 @@ export class InfraResolver {
userEmail, userEmail,
); );
} }
@Query(() => [InfraConfig], {
description: 'Retrieve configuration details for the instance',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async infraConfigs(
@Args({
name: 'configNames',
type: () => [InfraConfigEnumForClient],
description: 'Configs to fetch',
})
names: InfraConfigEnumForClient[],
) {
const infraConfigs = await this.infraConfigService.getMany(names);
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
return infraConfigs.right;
}
@Query(() => [String], {
description: 'Allowed Auth Provider list',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
allowedAuthProviders() {
return this.infraConfigService.getAllowedAuthProviders();
}
/* Mutations */
@Mutation(() => [InfraConfig], {
description: 'Update Infra Configs',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async updateInfraConfigs(
@Args({
name: 'infraConfigs',
type: () => [InfraConfigArgs],
description: 'InfraConfigs to update',
})
infraConfigs: InfraConfigArgs[],
) {
const updatedRes = await this.infraConfigService.updateMany(infraConfigs);
if (E.isLeft(updatedRes)) throwErr(updatedRes.left);
return updatedRes.right;
}
@Mutation(() => Boolean, {
description: 'Reset Infra Configs with default values (.env)',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async resetInfraConfigs() {
const resetRes = await this.infraConfigService.reset();
if (E.isLeft(resetRes)) throwErr(resetRes.left);
return true;
}
@Mutation(() => Boolean, {
description: 'Enable or Disable SSO for login/signup',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async enableAndDisableSSO(
@Args({
name: 'providerInfo',
type: () => [EnableAndDisableSSOArgs],
description: 'SSO provider and status',
})
providerInfo: EnableAndDisableSSOArgs[],
) {
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return true;
}
} }

View File

@@ -20,69 +20,51 @@ 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'; import { AppController } from './app.controller';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { InfraConfigModule } from './infra-config/infra-config.module';
import { loadInfraConfiguration } from './infra-config/helper';
import { MailerModule } from './mailer/mailer.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ GraphQLModule.forRoot<ApolloDriverConfig>({
isGlobal: true, buildSchemaOptions: {
load: [async () => loadInfraConfiguration()], numberScalarMode: 'integer',
}),
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
playground: configService.get('PRODUCTION') !== 'true',
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
'subscriptions-transport-ws': {
path: '/graphql',
onConnect: (_, websocket) => {
try {
const cookies = subscriptionContextCookieParser(
websocket.upgradeReq.headers.cookie,
);
return {
headers: { ...websocket?.upgradeReq?.headers, cookies },
};
} catch (error) {
throw new HttpException(COOKIES_NOT_FOUND, 400, {
cause: new Error(COOKIES_NOT_FOUND),
});
}
},
},
},
context: ({ req, res, connection }) => ({
req,
res,
connection,
}),
};
}, },
}), playground: process.env.PRODUCTION !== 'true',
ThrottlerModule.forRootAsync({ autoSchemaFile: true,
imports: [ConfigModule], installSubscriptionHandlers: true,
inject: [ConfigService], subscriptions: {
useFactory: async (configService: ConfigService) => [ 'subscriptions-transport-ws': {
{ path: '/graphql',
ttl: +configService.get('RATE_LIMIT_TTL'), onConnect: (_, websocket) => {
limit: +configService.get('RATE_LIMIT_MAX'), try {
const cookies = subscriptionContextCookieParser(
websocket.upgradeReq.headers.cookie,
);
return {
headers: { ...websocket?.upgradeReq?.headers, cookies },
};
} catch (error) {
throw new HttpException(COOKIES_NOT_FOUND, 400, {
cause: new Error(COOKIES_NOT_FOUND),
});
}
},
}, },
], },
context: ({ req, res, connection }) => ({
req,
res,
connection,
}),
driver: ApolloDriver,
}), }),
MailerModule.register(), ThrottlerModule.forRoot([
{
ttl: +process.env.RATE_LIMIT_TTL,
limit: +process.env.RATE_LIMIT_MAX,
},
]),
UserModule, UserModule,
AuthModule.register(), AuthModule,
AdminModule, AdminModule,
UserSettingsModule, UserSettingsModule,
UserEnvironmentsModule, UserEnvironmentsModule,
@@ -95,7 +77,6 @@ import { MailerModule } from './mailer/mailer.module';
TeamInvitationModule, TeamInvitationModule,
UserCollectionModule, UserCollectionModule,
ShortcodeModule, ShortcodeModule,
InfraConfigModule,
], ],
providers: [GQLComplexityPlugin], providers: [GQLComplexityPlugin],
controllers: [AppController], controllers: [AppController],

View File

@@ -2,6 +2,7 @@ import {
Body, Body,
Controller, Controller,
Get, Get,
InternalServerErrorException,
Post, Post,
Query, Query,
Request, Request,
@@ -30,21 +31,11 @@ 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'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@UseGuards(ThrottlerBehindProxyGuard) @UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' }) @Controller({ path: 'auth', version: '1' })
export class AuthController { export class AuthController {
constructor( constructor(private authService: AuthService) {}
private authService: AuthService,
private configService: ConfigService,
) {}
@Get('providers')
async getAuthProviders() {
const providers = await this.authService.getAuthProviders();
return { providers };
}
/** /**
** Route to initiate magic-link auth for a users email ** Route to initiate magic-link auth for a users email
@@ -54,14 +45,8 @@ export class AuthController {
@Body() authData: SignInMagicDto, @Body() authData: SignInMagicDto,
@Query('origin') origin: string, @Query('origin') origin: string,
) { ) {
if ( if (!authProviderCheck(AuthProvider.EMAIL))
!authProviderCheck(
AuthProvider.EMAIL,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 }); throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
const deviceIdToken = await this.authService.signInMagicLink( const deviceIdToken = await this.authService.signInMagicLink(
authData.email, authData.email,

View File

@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module'; import { UserModule } from 'src/user/user.module';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaModule } from 'src/prisma/prisma.module';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
@@ -11,47 +12,25 @@ 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'; import { AuthProvider, authProviderCheck } from './helper';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({ @Module({
imports: [ imports: [
PrismaModule, PrismaModule,
UserModule, UserModule,
MailerModule,
PassportModule, PassportModule,
JwtModule.registerAsync({ JwtModule.register({
imports: [ConfigModule], secret: process.env.JWT_SECRET,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
}),
}), }),
InfraConfigModule,
], ],
providers: [AuthService, JwtStrategy, RTJwtStrategy], providers: [
AuthService,
JwtStrategy,
RTJwtStrategy,
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
],
controllers: [AuthController], controllers: [AuthController],
}) })
export class AuthModule { export class AuthModule {}
static async register() {
const env = await loadInfraConfiguration();
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
const providers = [
...(authProviderCheck(AuthProvider.GOOGLE, allowedAuthProviders)
? [GoogleStrategy]
: []),
...(authProviderCheck(AuthProvider.GITHUB, allowedAuthProviders)
? [GithubStrategy]
: []),
...(authProviderCheck(AuthProvider.MICROSOFT, allowedAuthProviders)
? [MicrosoftStrategy]
: []),
];
return {
module: AuthModule,
providers,
};
}
}

View File

@@ -21,26 +21,15 @@ import { VerifyMagicDto } from './dto/verify-magic.dto';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockUser = mockDeep<UserService>(); const mockUser = mockDeep<UserService>();
const mockJWT = mockDeep<JwtService>(); const mockJWT = mockDeep<JwtService>();
const mockMailer = mockDeep<MailerService>(); const mockMailer = mockDeep<MailerService>();
const mockConfigService = mockDeep<ConfigService>();
const mockInfraConfigService = mockDeep<InfraConfigService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
const authService = new AuthService( const authService = new AuthService(mockUser, mockPrisma, mockJWT, mockMailer);
mockUser,
mockPrisma,
mockJWT,
mockMailer,
mockConfigService,
mockInfraConfigService,
);
const currentTime = new Date(); const currentTime = new Date();
@@ -102,8 +91,6 @@ describe('signInMagicLink', () => {
mockUser.createUserViaMagicLink.mockResolvedValue(user); mockUser.createUserViaMagicLink.mockResolvedValue(user);
// create new entry in VerificationToken table // create new entry in VerificationToken table
mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData); mockPrisma.verificationToken.create.mockResolvedValueOnce(passwordlessData);
// Read env variable 'MAGIC_LINK_TOKEN_VALIDITY' from config service
mockConfigService.get.mockReturnValue('3');
const result = await authService.signInMagicLink( const result = await authService.signInMagicLink(
'dwight@dundermifflin.com', 'dwight@dundermifflin.com',

View File

@@ -28,8 +28,6 @@ import { AuthError } from 'src/types/AuthError';
import { AuthUser, IsAdmin } from 'src/types/AuthUser'; import { AuthUser, IsAdmin } from 'src/types/AuthUser';
import { VerificationToken } from '@prisma/client'; import { VerificationToken } from '@prisma/client';
import { Origin } from './helper'; import { Origin } from './helper';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@@ -38,8 +36,6 @@ export class AuthService {
private prismaService: PrismaService, private prismaService: PrismaService,
private jwtService: JwtService, private jwtService: JwtService,
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly configService: ConfigService,
private infraConfigService: InfraConfigService,
) {} ) {}
/** /**
@@ -50,12 +46,10 @@ export class AuthService {
*/ */
private async generateMagicLinkTokens(user: AuthUser) { private async generateMagicLinkTokens(user: AuthUser) {
const salt = await bcrypt.genSalt( const salt = await bcrypt.genSalt(
parseInt(this.configService.get('TOKEN_SALT_COMPLEXITY')), parseInt(process.env.TOKEN_SALT_COMPLEXITY),
); );
const expiresOn = DateTime.now() const expiresOn = DateTime.now()
.plus({ .plus({ hours: parseInt(process.env.MAGIC_LINK_TOKEN_VALIDITY) })
hours: parseInt(this.configService.get('MAGIC_LINK_TOKEN_VALIDITY')),
})
.toISO() .toISO()
.toString(); .toString();
@@ -101,13 +95,13 @@ export class AuthService {
*/ */
private async generateRefreshToken(userUid: string) { private async generateRefreshToken(userUid: string) {
const refreshTokenPayload: RefreshTokenPayload = { const refreshTokenPayload: RefreshTokenPayload = {
iss: this.configService.get('VITE_BASE_URL'), iss: process.env.VITE_BASE_URL,
sub: userUid, sub: userUid,
aud: [this.configService.get('VITE_BASE_URL')], aud: [process.env.VITE_BASE_URL],
}; };
const refreshToken = await this.jwtService.sign(refreshTokenPayload, { const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
expiresIn: this.configService.get('REFRESH_TOKEN_VALIDITY'), //7 Days expiresIn: process.env.REFRESH_TOKEN_VALIDITY, //7 Days
}); });
const refreshTokenHash = await argon2.hash(refreshToken); const refreshTokenHash = await argon2.hash(refreshToken);
@@ -133,9 +127,9 @@ export class AuthService {
*/ */
async generateAuthTokens(userUid: string) { async generateAuthTokens(userUid: string) {
const accessTokenPayload: AccessTokenPayload = { const accessTokenPayload: AccessTokenPayload = {
iss: this.configService.get('VITE_BASE_URL'), iss: process.env.VITE_BASE_URL,
sub: userUid, sub: userUid,
aud: [this.configService.get('VITE_BASE_URL')], aud: [process.env.VITE_BASE_URL],
}; };
const refreshToken = await this.generateRefreshToken(userUid); const refreshToken = await this.generateRefreshToken(userUid);
@@ -143,7 +137,7 @@ export class AuthService {
return E.right(<AuthTokens>{ return E.right(<AuthTokens>{
access_token: await this.jwtService.sign(accessTokenPayload, { access_token: await this.jwtService.sign(accessTokenPayload, {
expiresIn: this.configService.get('ACCESS_TOKEN_VALIDITY'), //1 Day expiresIn: process.env.ACCESS_TOKEN_VALIDITY, //1 Day
}), }),
refresh_token: refreshToken.right, refresh_token: refreshToken.right,
}); });
@@ -224,14 +218,14 @@ export class AuthService {
let url: string; let url: string;
switch (origin) { switch (origin) {
case Origin.ADMIN: case Origin.ADMIN:
url = this.configService.get('VITE_ADMIN_URL'); url = process.env.VITE_ADMIN_URL;
break; break;
case Origin.APP: case Origin.APP:
url = this.configService.get('VITE_BASE_URL'); url = process.env.VITE_BASE_URL;
break; break;
default: default:
// if origin is invalid by default set URL to Hoppscotch-App // if origin is invalid by default set URL to Hoppscotch-App
url = this.configService.get('VITE_BASE_URL'); url = process.env.VITE_BASE_URL;
} }
await this.mailerService.sendEmail(email, { await this.mailerService.sendEmail(email, {
@@ -383,8 +377,4 @@ export class AuthService {
return E.right(<IsAdmin>{ isAdmin: false }); return E.right(<IsAdmin>{ isAdmin: false });
} }
getAuthProviders() {
return this.infraConfigService.getAllowedAuthProviders();
}
} }

View File

@@ -3,25 +3,14 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper'; import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate { export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate( canActivate(
context: ExecutionContext, context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> { ): boolean | Promise<boolean> | Observable<boolean> {
if ( if (!authProviderCheck(AuthProvider.GITHUB))
!authProviderCheck(
AuthProvider.GITHUB,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 }); throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
return super.canActivate(context); return super.canActivate(context);
} }

View File

@@ -3,25 +3,14 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper'; import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate { export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate( canActivate(
context: ExecutionContext, context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> { ): boolean | Promise<boolean> | Observable<boolean> {
if ( if (!authProviderCheck(AuthProvider.GOOGLE))
!authProviderCheck(
AuthProvider.GOOGLE,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 }); throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
}
return super.canActivate(context); return super.canActivate(context);
} }

View File

@@ -3,31 +3,20 @@ import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper'; import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors'; import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class MicrosoftSSOGuard export class MicrosoftSSOGuard
extends AuthGuard('microsoft') extends AuthGuard('microsoft')
implements CanActivate implements CanActivate
{ {
constructor(private readonly configService: ConfigService) {
super();
}
canActivate( canActivate(
context: ExecutionContext, context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> { ): boolean | Promise<boolean> | Observable<boolean> {
if ( if (!authProviderCheck(AuthProvider.MICROSOFT))
!authProviderCheck(
AuthProvider.MICROSOFT,
this.configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS'),
)
) {
throwHTTPErr({ throwHTTPErr({
message: AUTH_PROVIDER_NOT_SPECIFIED, message: AUTH_PROVIDER_NOT_SPECIFIED,
statusCode: 404, statusCode: 404,
}); });
}
return super.canActivate(context); return super.canActivate(context);
} }

View File

@@ -6,7 +6,6 @@ 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 { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { ConfigService } from '@nestjs/config';
enum AuthTokenType { enum AuthTokenType {
ACCESS_TOKEN = 'access_token', ACCESS_TOKEN = 'access_token',
@@ -46,17 +45,15 @@ export const authCookieHandler = (
redirect: boolean, redirect: boolean,
redirectUrl: string | null, redirectUrl: string | null,
) => { ) => {
const configService = new ConfigService();
const currentTime = DateTime.now(); const currentTime = DateTime.now();
const accessTokenValidity = currentTime const accessTokenValidity = currentTime
.plus({ .plus({
milliseconds: parseInt(configService.get('ACCESS_TOKEN_VALIDITY')), milliseconds: parseInt(process.env.ACCESS_TOKEN_VALIDITY),
}) })
.toMillis(); .toMillis();
const refreshTokenValidity = currentTime const refreshTokenValidity = currentTime
.plus({ .plus({
milliseconds: parseInt(configService.get('REFRESH_TOKEN_VALIDITY')), milliseconds: parseInt(process.env.REFRESH_TOKEN_VALIDITY),
}) })
.toMillis(); .toMillis();
@@ -78,12 +75,10 @@ export const authCookieHandler = (
} }
// check to see if redirectUrl is a whitelisted url // check to see if redirectUrl is a whitelisted url
const whitelistedOrigins = configService const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(',');
.get('WHITELISTED_ORIGINS')
.split(',');
if (!whitelistedOrigins.includes(redirectUrl)) if (!whitelistedOrigins.includes(redirectUrl))
// if it is not redirect by default to REDIRECT_URL // if it is not redirect by default to REDIRECT_URL
redirectUrl = configService.get('REDIRECT_URL'); redirectUrl = process.env.REDIRECT_URL;
return res.status(HttpStatus.OK).redirect(redirectUrl); return res.status(HttpStatus.OK).redirect(redirectUrl);
}; };
@@ -117,16 +112,13 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
* @param provider Provider we want to check the presence of * @param provider Provider we want to check the presence of
* @returns Boolean if provider specified is present or not * @returns Boolean if provider specified is present or not
*/ */
export function authProviderCheck( export function authProviderCheck(provider: string) {
provider: string,
VITE_ALLOWED_AUTH_PROVIDERS: string,
) {
if (!provider) { if (!provider) {
throwErr(AUTH_PROVIDER_NOT_SPECIFIED); throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
} }
const envVariables = VITE_ALLOWED_AUTH_PROVIDERS const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) => ? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
provider.trim().toUpperCase(), provider.trim().toUpperCase(),
) )
: []; : [];

View File

@@ -5,20 +5,18 @@ import { AuthService } from '../auth.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GithubStrategy extends PassportStrategy(Strategy) { export class GithubStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private usersService: UserService, private usersService: UserService,
private configService: ConfigService,
) { ) {
super({ super({
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'), clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'), clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: configService.get('GITHUB_CALLBACK_URL'), callbackURL: process.env.GITHUB_CALLBACK_URL,
scope: [configService.get('GITHUB_SCOPE')], scope: [process.env.GITHUB_SCOPE],
store: true, store: true,
}); });
} }

View File

@@ -5,20 +5,18 @@ import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) { export class GoogleStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private usersService: UserService, private usersService: UserService,
private authService: AuthService, private authService: AuthService,
private configService: ConfigService,
) { ) {
super({ super({
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'), clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'), clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: configService.get('GOOGLE_CALLBACK_URL'), callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: configService.get('GOOGLE_SCOPE').split(','), scope: process.env.GOOGLE_SCOPE.split(','),
passReqToCallback: true, passReqToCallback: true,
store: true, store: true,
}); });

View File

@@ -15,14 +15,10 @@ import {
INVALID_ACCESS_TOKEN, INVALID_ACCESS_TOKEN,
USER_NOT_FOUND, USER_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor( constructor(private usersService: UserService) {
private usersService: UserService,
private configService: ConfigService,
) {
super({ super({
jwtFromRequest: ExtractJwt.fromExtractors([ jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => { (request: Request) => {
@@ -33,7 +29,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
return ATCookie; return ATCookie;
}, },
]), ]),
secretOrKey: configService.get('JWT_SECRET'), secretOrKey: process.env.JWT_SECRET,
}); });
} }

View File

@@ -5,21 +5,19 @@ import { AuthService } from '../auth.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class MicrosoftStrategy extends PassportStrategy(Strategy) { export class MicrosoftStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private usersService: UserService, private usersService: UserService,
private configService: ConfigService,
) { ) {
super({ super({
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'), clientID: process.env.MICROSOFT_CLIENT_ID,
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'), clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'), callbackURL: process.env.MICROSOFT_CALLBACK_URL,
scope: [configService.get('MICROSOFT_SCOPE')], scope: [process.env.MICROSOFT_SCOPE],
tenant: configService.get('MICROSOFT_TENANT'), tenant: process.env.MICROSOFT_TENANT,
store: true, store: true,
}); });
} }

View File

@@ -14,14 +14,10 @@ import {
USER_NOT_FOUND, USER_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor( constructor(private usersService: UserService) {
private usersService: UserService,
private configService: ConfigService,
) {
super({ super({
jwtFromRequest: ExtractJwt.fromExtractors([ jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => { (request: Request) => {
@@ -32,7 +28,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
return RTCookie; return RTCookie;
}, },
]), ]),
secretOrKey: configService.get('JWT_SECRET'), secretOrKey: process.env.JWT_SECRET,
}); });
} }

View File

@@ -254,13 +254,6 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
*/ */
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const; export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
/**
* The Team Collection data is not valid
* (TeamCollectionService)
*/
export const TEAM_COLL_DATA_INVALID =
'team_coll/team_coll_data_invalid' as const;
/** /**
* Tried to perform an action on a request that doesn't accept their member role level * Tried to perform an action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard) * (GqlRequestTeamMemberGuard)
@@ -592,13 +585,6 @@ export const USER_COLL_REORDERING_FAILED =
export const USER_COLL_SAME_NEXT_COLL = export const USER_COLL_SAME_NEXT_COLL =
'user_coll/user_collection_and_next_user_collection_are_same' as const; 'user_coll/user_collection_and_next_user_collection_are_same' as const;
/**
* The User Collection data is not valid
* (UserCollectionService)
*/
export const USER_COLL_DATA_INVALID =
'user_coll/user_coll_data_invalid' as const;
/** /**
* The User Collection does not belong to the logged-in user * The User Collection does not belong to the logged-in user
* (UserCollectionService) * (UserCollectionService)
@@ -644,41 +630,3 @@ export const SHORTCODE_INVALID_PROPERTIES_JSON =
*/ */
export const SHORTCODE_PROPERTIES_NOT_FOUND = export const SHORTCODE_PROPERTIES_NOT_FOUND =
'shortcode/properties_not_found' as const; 'shortcode/properties_not_found' as const;
/**
* Infra Config not found
* (InfraConfigService)
*/
export const INFRA_CONFIG_NOT_FOUND = 'infra_config/not_found' as const;
/**
* Infra Config update failed
* (InfraConfigService)
*/
export const INFRA_CONFIG_UPDATE_FAILED = 'infra_config/update_failed' as const;
/**
* Infra Config not listed for onModuleInit creation
* (InfraConfigService)
*/
export const INFRA_CONFIG_NOT_LISTED =
'infra_config/properly_not_listed' as const;
/**
* Infra Config reset failed
* (InfraConfigService)
*/
export const INFRA_CONFIG_RESET_FAILED = 'infra_config/reset_failed' as const;
/**
* Infra Config invalid input for Config variable
* (InfraConfigService)
*/
export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
/**
* Error message for when the database table does not exist
* (InfraConfigService)
*/
export const DATABASE_TABLE_NOT_EXIST =
'Database migration not performed. Please check the FAQ for assistance: https://docs.hoppscotch.io/support/faq';

View File

@@ -1,44 +0,0 @@
import { PrismaService } from 'src/prisma/prisma.service';
export enum ServiceStatus {
ENABLE = 'ENABLE',
DISABLE = 'DISABLE',
}
/**
* Load environment variables from the database and set them in the process
*
* @Description Fetch the 'infra_config' table from the database and return it as an object
* (ConfigModule will set the environment variables in the process)
*/
export async function loadInfraConfiguration() {
try {
const prisma = new PrismaService();
const infraConfigs = await prisma.infraConfig.findMany();
let environmentObject: Record<string, any> = {};
infraConfigs.forEach((infraConfig) => {
environmentObject[infraConfig.name] = infraConfig.value;
});
return { INFRA: environmentObject };
} catch (error) {
// Prisma throw error if 'Can't reach at database server' OR 'Table does not exist'
// Reason for not throwing error is, we want successful build during 'postinstall' and generate dist files
return { INFRA: {} };
}
}
/**
* Stop the app after 5 seconds
* (Docker will re-start the app)
*/
export function stopApp() {
console.log('Stopping app in 5 seconds...');
setTimeout(() => {
console.log('Stopping app now...');
process.kill(process.pid, 'SIGTERM');
}, 5000);
}

View File

@@ -1,29 +0,0 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { AuthProvider } from 'src/auth/helper';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
import { ServiceStatus } from './helper';
@ObjectType()
export class InfraConfig {
@Field({
description: 'Infra Config Name',
})
name: InfraConfigEnumForClient;
@Field({
description: 'Infra Config Value',
})
value: string;
}
registerEnumType(InfraConfigEnumForClient, {
name: 'InfraConfigEnum',
});
registerEnumType(AuthProvider, {
name: 'AuthProvider',
});
registerEnumType(ServiceStatus, {
name: 'ServiceStatus',
});

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { InfraConfigService } from './infra-config.service';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [InfraConfigService],
exports: [InfraConfigService],
})
export class InfraConfigModule {}

View File

@@ -1,109 +0,0 @@
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfigService } from './infra-config.service';
import {
InfraConfigEnum,
InfraConfigEnumForClient,
} from 'src/types/InfraConfig';
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import * as helper from './helper';
const mockPrisma = mockDeep<PrismaService>();
const mockConfigService = mockDeep<ConfigService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const infraConfigService = new InfraConfigService(
mockPrisma,
mockConfigService,
);
beforeEach(() => {
mockReset(mockPrisma);
});
describe('InfraConfigService', () => {
describe('update', () => {
it('should update the infra config', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.update.mockResolvedValueOnce({
id: '',
name,
value,
active: true,
createdOn: new Date(),
updatedOn: new Date(),
});
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
const result = await infraConfigService.update(name, value);
expect(result).toEqualRight({ name, value });
});
it('should pass correct params to prisma update', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
await infraConfigService.update(name, value);
expect(mockPrisma.infraConfig.update).toHaveBeenCalledWith({
where: { name },
data: { value },
});
expect(mockPrisma.infraConfig.update).toHaveBeenCalledTimes(1);
});
it('should throw an error if the infra config update failed', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.update.mockRejectedValueOnce('null');
const result = await infraConfigService.update(name, value);
expect(result).toEqualLeft(INFRA_CONFIG_UPDATE_FAILED);
});
});
describe('get', () => {
it('should get the infra config', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
id: '',
name,
value,
active: true,
createdOn: new Date(),
updatedOn: new Date(),
});
const result = await infraConfigService.get(name);
expect(result).toEqualRight({ name, value });
});
it('should pass correct params to prisma findUnique', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
await infraConfigService.get(name);
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledWith({
where: { name },
});
expect(mockPrisma.infraConfig.findUniqueOrThrow).toHaveBeenCalledTimes(1);
});
it('should throw an error if the infra config does not exist', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
const result = await infraConfigService.get(name);
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
});
});
});

View File

@@ -1,312 +0,0 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { InfraConfig } from './infra-config.model';
import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfig as DBInfraConfig } from '@prisma/client';
import * as E from 'fp-ts/Either';
import {
InfraConfigEnum,
InfraConfigEnumForClient,
} from 'src/types/InfraConfig';
import {
AUTH_PROVIDER_NOT_SPECIFIED,
DATABASE_TABLE_NOT_EXIST,
INFRA_CONFIG_INVALID_INPUT,
INFRA_CONFIG_NOT_FOUND,
INFRA_CONFIG_NOT_LISTED,
INFRA_CONFIG_RESET_FAILED,
INFRA_CONFIG_UPDATE_FAILED,
} from 'src/errors';
import { throwErr, validateEmail, validateSMTPUrl } from 'src/utils';
import { ConfigService } from '@nestjs/config';
import { ServiceStatus, stopApp } from './helper';
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
@Injectable()
export class InfraConfigService implements OnModuleInit {
constructor(
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
) {}
async onModuleInit() {
await this.initializeInfraConfigTable();
}
getDefaultInfraConfigs(): { name: InfraConfigEnum; value: string }[] {
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
{
name: InfraConfigEnum.MAILER_SMTP_URL,
value: process.env.MAILER_SMTP_URL,
},
{
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
value: process.env.MAILER_ADDRESS_FROM,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: process.env.GOOGLE_CLIENT_ID,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
value: process.env.GOOGLE_CLIENT_SECRET,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_ID,
value: process.env.GITHUB_CLIENT_ID,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
value: process.env.GITHUB_CLIENT_SECRET,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
value: process.env.MICROSOFT_CLIENT_ID,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
value: process.env.MICROSOFT_CLIENT_SECRET,
},
{
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
value: process.env.VITE_ALLOWED_AUTH_PROVIDERS.toLocaleUpperCase(),
},
];
return infraConfigDefaultObjs;
}
/**
* Initialize the 'infra_config' table with values from .env
* @description This function create rows 'infra_config' in very first time (only once)
*/
async initializeInfraConfigTable() {
try {
// Get all the 'names' of the properties to be saved in the 'infra_config' table
const enumValues = Object.values(InfraConfigEnum);
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
// Check if all the 'names' are listed in the default values
if (enumValues.length !== infraConfigDefaultObjs.length) {
throw new Error(INFRA_CONFIG_NOT_LISTED);
}
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
const propsToInsert = infraConfigDefaultObjs.filter(
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
);
if (propsToInsert.length > 0) {
await this.prisma.infraConfig.createMany({ data: propsToInsert });
stopApp();
}
} catch (error) {
if (error.code === 'P1001') {
// Prisma error code for 'Can't reach at database server'
// We're not throwing error here because we want to allow the app to run 'pnpm install'
} else if (error.code === 'P2021') {
// Prisma error code for 'Table does not exist'
throwErr(DATABASE_TABLE_NOT_EXIST);
} else {
throwErr(error);
}
}
}
/**
* Typecast a database InfraConfig to a InfraConfig model
* @param dbInfraConfig database InfraConfig
* @returns InfraConfig model
*/
cast(dbInfraConfig: DBInfraConfig) {
return <InfraConfig>{
name: dbInfraConfig.name,
value: dbInfraConfig.value,
};
}
/**
* Update InfraConfig by name
* @param name Name of the InfraConfig
* @param value Value of the InfraConfig
* @returns InfraConfig model
*/
async update(
name: InfraConfigEnumForClient | InfraConfigEnum,
value: string,
) {
const isValidate = this.validateEnvValues([{ name, value }]);
if (E.isLeft(isValidate)) return E.left(isValidate.left);
try {
const infraConfig = await this.prisma.infraConfig.update({
where: { name },
data: { value },
});
stopApp();
return E.right(this.cast(infraConfig));
} catch (e) {
return E.left(INFRA_CONFIG_UPDATE_FAILED);
}
}
/**
* Update InfraConfigs by name
* @param infraConfigs InfraConfigs to update
* @returns InfraConfig model
*/
async updateMany(infraConfigs: InfraConfigArgs[]) {
const isValidate = this.validateEnvValues(infraConfigs);
if (E.isLeft(isValidate)) return E.left(isValidate.left);
try {
await this.prisma.$transaction(async (tx) => {
for (let i = 0; i < infraConfigs.length; i++) {
await tx.infraConfig.update({
where: { name: infraConfigs[i].name },
data: { value: infraConfigs[i].value },
});
}
});
stopApp();
return E.right(infraConfigs);
} catch (e) {
return E.left(INFRA_CONFIG_UPDATE_FAILED);
}
}
/**
* Enable or Disable SSO for login/signup
* @param provider Auth Provider to enable or disable
* @param status Status to enable or disable
* @returns Either true or an error
*/
async enableAndDisableSSO(providerInfo: EnableAndDisableSSOArgs[]) {
const allowedAuthProviders = this.configService
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
.split(',');
let updatedAuthProviders = allowedAuthProviders;
providerInfo.forEach(({ provider, status }) => {
if (status === ServiceStatus.ENABLE) {
updatedAuthProviders.push(provider);
} else if (status === ServiceStatus.DISABLE) {
updatedAuthProviders = updatedAuthProviders.filter(
(p) => p !== provider,
);
}
});
updatedAuthProviders = [...new Set(updatedAuthProviders)];
if (updatedAuthProviders.length === 0) {
return E.left(AUTH_PROVIDER_NOT_SPECIFIED);
}
const isUpdated = await this.update(
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
updatedAuthProviders.join(','),
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
return E.right(true);
}
/**
* Get InfraConfig by name
* @param name Name of the InfraConfig
* @returns InfraConfig model
*/
async get(name: InfraConfigEnumForClient) {
try {
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
where: { name },
});
return E.right(this.cast(infraConfig));
} catch (e) {
return E.left(INFRA_CONFIG_NOT_FOUND);
}
}
/**
* Get InfraConfigs by names
* @param names Names of the InfraConfigs
* @returns InfraConfig model
*/
async getMany(names: InfraConfigEnumForClient[]) {
try {
const infraConfigs = await this.prisma.infraConfig.findMany({
where: { name: { in: names } },
});
return E.right(infraConfigs.map((p) => this.cast(p)));
} catch (e) {
return E.left(INFRA_CONFIG_NOT_FOUND);
}
}
/**
* Get allowed auth providers for login/signup
* @returns string[]
*/
getAllowedAuthProviders() {
return this.configService
.get<string>('INFRA.VITE_ALLOWED_AUTH_PROVIDERS')
.split(',');
}
/**
* Reset all the InfraConfigs to their default values (from .env)
*/
async reset() {
try {
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
await this.prisma.infraConfig.deleteMany({
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
});
await this.prisma.infraConfig.createMany({
data: infraConfigDefaultObjs,
});
stopApp();
return E.right(true);
} catch (e) {
return E.left(INFRA_CONFIG_RESET_FAILED);
}
}
validateEnvValues(
infraConfigs: {
name: InfraConfigEnumForClient | InfraConfigEnum;
value: string;
}[],
) {
for (let i = 0; i < infraConfigs.length; i++) {
switch (infraConfigs[i].name) {
case InfraConfigEnumForClient.MAILER_SMTP_URL:
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
const isValidEmail = validateEmail(infraConfigs[i].value);
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
default:
break;
}
}
return E.right(true);
}
}

View File

@@ -1,30 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
import { ServiceStatus } from './helper';
import { AuthProvider } from 'src/auth/helper';
@InputType()
export class InfraConfigArgs {
@Field(() => InfraConfigEnumForClient, {
description: 'Infra Config Name',
})
name: InfraConfigEnumForClient;
@Field({
description: 'Infra Config Value',
})
value: string;
}
@InputType()
export class EnableAndDisableSSOArgs {
@Field(() => AuthProvider, {
description: 'Auth Provider',
})
provider: AuthProvider;
@Field(() => ServiceStatus, {
description: 'Auth Provider Status',
})
status: ServiceStatus;
}

View File

@@ -1,4 +1,4 @@
import { Global, Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer'; import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { MailerService } from './mailer.service'; import { MailerService } from './mailer.service';
@@ -7,42 +7,24 @@ import {
MAILER_FROM_ADDRESS_UNDEFINED, MAILER_FROM_ADDRESS_UNDEFINED,
MAILER_SMTP_URL_UNDEFINED, MAILER_SMTP_URL_UNDEFINED,
} from 'src/errors'; } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
@Global()
@Module({ @Module({
imports: [], imports: [
NestMailerModule.forRoot({
transport:
process.env.MAILER_SMTP_URL ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from:
process.env.MAILER_ADDRESS_FROM ??
throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
},
}),
],
providers: [MailerService], providers: [MailerService],
exports: [MailerService], exports: [MailerService],
}) })
export class MailerModule { export class MailerModule {}
static async register() {
const env = await loadInfraConfiguration();
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
const config = new ConfigService();
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
}
return {
module: MailerModule,
imports: [
NestMailerModule.forRoot({
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',
adapter: new HandlebarsAdapter(),
},
}),
],
};
}
}

View File

@@ -6,23 +6,18 @@ 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'; import { checkEnvironmentAuthProvider } from './utils';
import { ConfigService } from '@nestjs/config';
async function bootstrap() { async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`);
checkEnvironmentAuthProvider();
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
console.log(`Running in production: ${configService.get('PRODUCTION')}`);
console.log(`Port: ${configService.get('PORT')}`);
checkEnvironmentAuthProvider(
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
);
app.use( app.use(
session({ session({
secret: configService.get('SESSION_SECRET'), secret: process.env.SESSION_SECRET,
}), }),
); );
@@ -33,18 +28,18 @@ async function bootstrap() {
}), }),
); );
if (configService.get('PRODUCTION') === 'false') { if (process.env.PRODUCTION === 'false') {
console.log('Enabling CORS with development settings'); console.log('Enabling CORS with development settings');
app.enableCors({ app.enableCors({
origin: configService.get('WHITELISTED_ORIGINS').split(','), origin: process.env.WHITELISTED_ORIGINS.split(','),
credentials: true, credentials: true,
}); });
} else { } else {
console.log('Enabling CORS with production settings'); console.log('Enabling CORS with production settings');
app.enableCors({ app.enableCors({
origin: configService.get('WHITELISTED_ORIGINS').split(','), origin: process.env.WHITELISTED_ORIGINS.split(','),
credentials: true, credentials: true,
}); });
} }
@@ -52,13 +47,7 @@ async function bootstrap() {
type: VersioningType.URI, type: VersioningType.URI,
}); });
app.use(cookieParser()); app.use(cookieParser());
await app.listen(configService.get('PORT') || 3170); await app.listen(process.env.PORT || 3170);
// Graceful shutdown
process.on('SIGTERM', async () => {
console.info('SIGTERM signal received');
await app.close();
});
} }
if (!process.env.GENERATE_GQL_SCHEMA) { if (!process.env.GENERATE_GQL_SCHEMA) {

View File

@@ -21,8 +21,8 @@ import {
} from 'src/team-request/team-request.model'; } from 'src/team-request/team-request.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { InvitedUser } from '../admin/invited-user.model'; import { InvitedUser } from '../admin/invited-user.model';
import { UserCollection } from '@prisma/client';
import { import {
UserCollection,
UserCollectionRemovedData, UserCollectionRemovedData,
UserCollectionReorderData, UserCollectionReorderData,
} from 'src/user-collection/user-collections.model'; } from 'src/user-collection/user-collections.model';

View File

@@ -504,24 +504,20 @@ describe('ShortcodeService', () => {
); );
expect(result).toEqual(<ShortcodeWithUserEmail[]>[ expect(result).toEqual(<ShortcodeWithUserEmail[]>[
{ {
id: shortcodesWithUserEmail[0].id, id: shortcodes[0].id,
request: JSON.stringify(shortcodesWithUserEmail[0].request), request: JSON.stringify(shortcodes[0].request),
properties: JSON.stringify( properties: JSON.stringify(shortcodes[0].embedProperties),
shortcodesWithUserEmail[0].embedProperties, createdOn: shortcodes[0].createdOn,
),
createdOn: shortcodesWithUserEmail[0].createdOn,
creator: { creator: {
uid: user.uid, uid: user.uid,
email: user.email, email: user.email,
}, },
}, },
{ {
id: shortcodesWithUserEmail[1].id, id: shortcodes[1].id,
request: JSON.stringify(shortcodesWithUserEmail[1].request), request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify( properties: JSON.stringify(shortcodes[1].embedProperties),
shortcodesWithUserEmail[1].embedProperties, createdOn: shortcodes[1].createdOn,
),
createdOn: shortcodesWithUserEmail[1].createdOn,
creator: { creator: {
uid: user.uid, uid: user.uid,
email: user.email, email: user.email,

View File

@@ -14,13 +14,6 @@ export class CreateRootTeamCollectionArgs {
@Field({ name: 'title', description: 'Title of the new collection' }) @Field({ name: 'title', description: 'Title of the new collection' })
title: string; title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
@@ -33,13 +26,6 @@ export class CreateChildTeamCollectionArgs {
@Field({ name: 'childTitle', description: 'Title of the new collection' }) @Field({ name: 'childTitle', description: 'Title of the new collection' })
childTitle: string; childTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
@@ -47,14 +33,12 @@ export class RenameTeamCollectionArgs {
@Field(() => ID, { @Field(() => ID, {
name: 'collectionID', name: 'collectionID',
description: 'ID of the collection', description: 'ID of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
}) })
collectionID: string; collectionID: string;
@Field({ @Field({
name: 'newTitle', name: 'newTitle',
description: 'The updated title of the collection', description: 'The updated title of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
}) })
newTitle: string; newTitle: string;
} }
@@ -114,26 +98,3 @@ export class ReplaceTeamCollectionArgs {
}) })
parentCollectionID?: string; parentCollectionID?: string;
} }
@ArgsType()
export class UpdateTeamCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of the collection',
})
collectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -12,17 +12,12 @@ export class TeamCollection {
}) })
title: string; title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ID, { @Field(() => ID, {
description: 'ID of the collection', description: 'ID of the collection',
nullable: true, nullable: true,
}) })
parentID: string; parentID: string;
teamID: string;
} }
@ObjectType() @ObjectType()

View File

@@ -25,7 +25,6 @@ import {
MoveTeamCollectionArgs, MoveTeamCollectionArgs,
RenameTeamCollectionArgs, RenameTeamCollectionArgs,
ReplaceTeamCollectionArgs, ReplaceTeamCollectionArgs,
UpdateTeamCollectionArgs,
UpdateTeamCollectionOrderArgs, UpdateTeamCollectionOrderArgs,
} from './input-type.args'; } from './input-type.args';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
@@ -142,14 +141,7 @@ export class TeamCollectionResolver {
); );
if (E.isLeft(teamCollections)) throwErr(teamCollections.left); if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
return <TeamCollection>{ return teamCollections.right;
id: teamCollections.right.id,
title: teamCollections.right.title,
parentID: teamCollections.right.parentID,
data: !teamCollections.right.data
? null
: JSON.stringify(teamCollections.right.data),
};
} }
// Mutations // Mutations
@@ -163,7 +155,6 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection( const teamCollection = await this.teamCollectionService.createCollection(
args.teamID, args.teamID,
args.title, args.title,
args.data,
null, null,
); );
@@ -239,7 +230,6 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection( const teamCollection = await this.teamCollectionService.createCollection(
team.right.id, team.right.id,
args.childTitle, args.childTitle,
args.data,
args.collectionID, args.collectionID,
); );
@@ -249,7 +239,6 @@ export class TeamCollectionResolver {
@Mutation(() => TeamCollection, { @Mutation(() => TeamCollection, {
description: 'Rename a collection', description: 'Rename a collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
}) })
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
@@ -314,23 +303,6 @@ export class TeamCollectionResolver {
return request.right; return request.right;
} }
@Mutation(() => TeamCollection, {
description: 'Update Team Collection details',
})
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async updateTeamCollection(@Args() args: UpdateTeamCollectionArgs) {
const updatedTeamCollection =
await this.teamCollectionService.updateTeamCollection(
args.collectionID,
args.data,
args.newTitle,
);
if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left);
return updatedTeamCollection.right;
}
// Subscriptions // Subscriptions
@Subscription(() => TeamCollection, { @Subscription(() => TeamCollection, {

View File

@@ -1,7 +1,6 @@
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client'; import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import { import {
TEAM_COLL_DATA_INVALID,
TEAM_COLL_DEST_SAME, TEAM_COLL_DEST_SAME,
TEAM_COLL_INVALID_JSON, TEAM_COLL_INVALID_JSON,
TEAM_COLL_IS_PARENT_COLL, TEAM_COLL_IS_PARENT_COLL,
@@ -18,7 +17,6 @@ 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';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>(); const mockPubSub = mockDeep<PubSubService>();
@@ -53,60 +51,35 @@ const rootTeamCollection: DBTeamCollection = {
id: '123', id: '123',
orderIndex: 1, orderIndex: 1,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}; };
const rootTeamCollectionsCasted: TeamCollection = {
id: rootTeamCollection.id,
title: rootTeamCollection.title,
parentID: rootTeamCollection.parentID,
data: JSON.stringify(rootTeamCollection.data),
};
const rootTeamCollection_2: DBTeamCollection = { const rootTeamCollection_2: DBTeamCollection = {
id: 'erv', id: 'erv',
orderIndex: 2, orderIndex: 2,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}; };
const rootTeamCollection_2Casted: TeamCollection = {
id: 'erv',
parentID: null,
data: JSON.stringify(rootTeamCollection_2.data),
title: 'Root Collection 1',
};
const childTeamCollection: DBTeamCollection = { const childTeamCollection: DBTeamCollection = {
id: 'rfe', id: 'rfe',
orderIndex: 1, orderIndex: 1,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Child Collection 1', title: 'Child Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}; };
const childTeamCollectionCasted: TeamCollection = {
id: 'rfe',
parentID: rootTeamCollection.id,
data: JSON.stringify(childTeamCollection.data),
title: 'Child Collection 1',
};
const childTeamCollection_2: DBTeamCollection = { const childTeamCollection_2: DBTeamCollection = {
id: 'bgdz', id: 'bgdz',
orderIndex: 1, orderIndex: 1,
data: {},
parentID: rootTeamCollection_2.id, parentID: rootTeamCollection_2.id,
title: 'Child Collection 1', title: 'Child Collection 1',
teamID: team.id, teamID: team.id,
@@ -114,20 +87,11 @@ const childTeamCollection_2: DBTeamCollection = {
updatedOn: currentTime, updatedOn: currentTime,
}; };
const childTeamCollection_2Casted: TeamCollection = {
id: 'bgdz',
data: JSON.stringify(childTeamCollection_2.data),
parentID: rootTeamCollection_2.id,
title: 'Child Collection 1',
};
const rootTeamCollectionList: DBTeamCollection[] = [ const rootTeamCollectionList: DBTeamCollection[] = [
{ {
id: 'fdv', id: 'fdv',
orderIndex: 1, orderIndex: 1,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -138,8 +102,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2, orderIndex: 2,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -149,8 +111,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3, orderIndex: 3,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -159,8 +119,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
id: 'bre3', id: 'bre3',
orderIndex: 4, orderIndex: 4,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -171,8 +129,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 5, orderIndex: 5,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -183,8 +139,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
data: {},
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}, },
@@ -194,8 +148,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
data: {},
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}, },
@@ -204,7 +156,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 8, orderIndex: 8,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -214,7 +165,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 9, orderIndex: 9,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -225,83 +175,17 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
data: {},
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}, },
]; ];
const rootTeamCollectionListCasted: TeamCollection[] = [
{
id: 'fdv',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fbbg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fgbfg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'bre3',
parentID: null,
data: JSON.stringify(rootTeamCollection.data),
title: 'Root Collection 1',
},
{
id: 'hghgf',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '123',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '54tyh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '234re',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '34rtg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '45tgh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
];
const childTeamCollectionList: DBTeamCollection[] = [ const childTeamCollectionList: DBTeamCollection[] = [
{ {
id: '123', id: '123',
orderIndex: 1, orderIndex: 1,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -311,8 +195,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2, orderIndex: 2,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -322,8 +204,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3, orderIndex: 3,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -332,8 +212,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '567', id: '567',
orderIndex: 4, orderIndex: 4,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -343,8 +221,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '123', id: '123',
orderIndex: 5, orderIndex: 5,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -354,8 +230,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '678', id: '678',
orderIndex: 6, orderIndex: 6,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -365,8 +239,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '789', id: '789',
orderIndex: 7, orderIndex: 7,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -376,8 +248,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '890', id: '890',
orderIndex: 8, orderIndex: 8,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -387,7 +257,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '012', id: '012',
orderIndex: 9, orderIndex: 9,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -397,8 +266,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '0bhu', id: '0bhu',
orderIndex: 10, orderIndex: 10,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -406,75 +273,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
}, },
]; ];
const childTeamCollectionListCasted: TeamCollection[] = [
{
id: '123',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '345',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '456',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '567',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '123',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '678',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '789',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '890',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '012',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '0bhu',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
];
beforeEach(() => { beforeEach(() => {
mockReset(mockPrisma); mockReset(mockPrisma);
mockPubSub.publish.mockClear(); mockPubSub.publish.mockClear();
@@ -513,7 +311,7 @@ describe('getParentOfCollection', () => {
const result = await teamCollectionService.getParentOfCollection( const result = await teamCollectionService.getParentOfCollection(
childTeamCollection.id, childTeamCollection.id,
); );
expect(result).toEqual(rootTeamCollectionsCasted); expect(result).toEqual(rootTeamCollection);
}); });
test('should return null successfully for a root collection with valid collectionID', async () => { test('should return null successfully for a root collection with valid collectionID', async () => {
@@ -549,7 +347,7 @@ describe('getChildrenOfCollection', () => {
null, null,
10, 10,
); );
expect(result).toEqual(childTeamCollectionListCasted); expect(result).toEqual(childTeamCollectionList);
}); });
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => { test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -565,9 +363,9 @@ describe('getChildrenOfCollection', () => {
10, 10,
); );
expect(result).toEqual([ expect(result).toEqual([
{ ...childTeamCollectionListCasted[7] }, { ...childTeamCollectionList[7] },
{ ...childTeamCollectionListCasted[8] }, { ...childTeamCollectionList[8] },
{ ...childTeamCollectionListCasted[9] }, { ...childTeamCollectionList[9] },
]); ]);
}); });
@@ -594,7 +392,7 @@ describe('getTeamRootCollections', () => {
null, null,
10, 10,
); );
expect(result).toEqual(rootTeamCollectionListCasted); expect(result).toEqual(rootTeamCollectionList);
}); });
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => { test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -610,9 +408,9 @@ describe('getTeamRootCollections', () => {
10, 10,
); );
expect(result).toEqual([ expect(result).toEqual([
{ ...rootTeamCollectionListCasted[7] }, { ...rootTeamCollectionList[7] },
{ ...rootTeamCollectionListCasted[8] }, { ...rootTeamCollectionList[8] },
{ ...rootTeamCollectionListCasted[9] }, { ...rootTeamCollectionList[9] },
]); ]);
}); });
@@ -666,7 +464,6 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'ab', 'ab',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE); expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
@@ -681,27 +478,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'abcd', 'abcd',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualLeft(TEAM_NOT_OWNER); expect(result).toEqualLeft(TEAM_NOT_OWNER);
}); });
test('should throw TEAM_COLL_DATA_INVALID when parent TeamCollection does not belong to the team', async () => {
// isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
rootTeamCollection,
);
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcd',
'{',
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should successfully create a new root TeamCollection with valid inputs', async () => { test('should successfully create a new root TeamCollection with valid inputs', async () => {
// isOwnerCheck // isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
@@ -715,10 +496,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'abcdefg', 'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualRight(rootTeamCollectionsCasted); expect(result).toEqualRight(rootTeamCollection);
}); });
test('should successfully create a new child TeamCollection with valid inputs', async () => { test('should successfully create a new child TeamCollection with valid inputs', async () => {
@@ -734,10 +514,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
childTeamCollection.teamID, childTeamCollection.teamID,
childTeamCollection.title, childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualRight(childTeamCollectionCasted); expect(result).toEqualRight(childTeamCollection);
}); });
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => { test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
@@ -753,13 +532,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
childTeamCollection.teamID, childTeamCollection.teamID,
childTeamCollection.title, childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_added`, `team_coll/${childTeamCollection.teamID}/coll_added`,
childTeamCollectionCasted, childTeamCollection,
); );
}); });
@@ -776,13 +553,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'abcdefg', 'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`, `team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted, rootTeamCollection,
); );
}); });
}); });
@@ -812,7 +587,7 @@ describe('renameCollection', () => {
'NewTitle', 'NewTitle',
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...rootTeamCollectionsCasted, ...rootTeamCollection,
title: 'NewTitle', title: 'NewTitle',
}); });
}); });
@@ -850,7 +625,7 @@ describe('renameCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`, `team_coll/${rootTeamCollection.teamID}/coll_updated`,
{ {
...rootTeamCollectionsCasted, ...rootTeamCollection,
title: 'NewTitle', title: 'NewTitle',
}, },
); );
@@ -1057,8 +832,9 @@ describe('moveCollection', () => {
null, null,
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...childTeamCollectionCasted, ...childTeamCollection,
parentID: null, parentID: null,
orderIndex: 2,
}); });
}); });
@@ -1114,8 +890,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`, `team_coll/${childTeamCollection.teamID}/coll_moved`,
{ {
...childTeamCollectionCasted, ...childTeamCollection,
parentID: null, parentID: null,
orderIndex: 2,
}, },
); );
}); });
@@ -1154,8 +931,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id, childTeamCollection_2.id,
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...rootTeamCollectionsCasted, ...rootTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}); });
}); });
@@ -1195,8 +973,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection_2.teamID}/coll_moved`, `team_coll/${childTeamCollection_2.teamID}/coll_moved`,
{ {
...rootTeamCollectionsCasted, ...rootTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}, },
); );
}); });
@@ -1235,8 +1014,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id, childTeamCollection_2.id,
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...childTeamCollectionCasted, ...childTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}); });
}); });
@@ -1276,8 +1056,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`, `team_coll/${childTeamCollection.teamID}/coll_moved`,
{ {
...childTeamCollectionCasted, ...childTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}, },
); );
}); });
@@ -1373,7 +1154,7 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`, `team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
{ {
collection: rootTeamCollectionListCasted[4], collection: rootTeamCollectionList[4],
nextCollection: null, nextCollection: null,
}, },
); );
@@ -1454,8 +1235,8 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`, `team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
{ {
collection: childTeamCollectionListCasted[4], collection: childTeamCollectionList[4],
nextCollection: childTeamCollectionListCasted[2], nextCollection: childTeamCollectionList[2],
}, },
); );
}); });
@@ -1521,7 +1302,7 @@ describe('importCollectionsFromJSON', () => {
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`, `team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted, rootTeamCollection,
); );
}); });
}); });
@@ -1640,7 +1421,7 @@ describe('replaceCollectionsWithJSON', () => {
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`, `team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted, rootTeamCollection,
); );
}); });
}); });
@@ -1677,64 +1458,4 @@ describe('totalCollectionsInTeam', () => {
}); });
}); });
describe('updateTeamCollection', () => {
test('should throw TEAM_COLL_SHORT_TITLE if title is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
'de',
);
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
});
test('should throw TEAM_COLL_DATA_INVALID is collection data is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
'{',
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should throw TEAM_COLL_NOT_FOUND is collectionID is invalid', async () => {
mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound');
const result = await teamCollectionService.updateTeamCollection(
'invalid_id',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
});
test('should successfully update a collection', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify({ foo: 'bar' }),
'new_title',
);
expect(result).toEqualRight({
data: JSON.stringify({ foo: 'bar' }),
title: 'new_title',
...rootTeamCollectionsCasted,
});
});
test('should send pubsub message to "team_coll/<teamID>/coll_updated" if TeamCollection is updated successfully', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
rootTeamCollectionsCasted,
);
});
});
//ToDo: write test cases for exportCollectionsToJSON //ToDo: write test cases for exportCollectionsToJSON

View File

@@ -13,7 +13,6 @@ import {
TEAM_COLL_IS_PARENT_COLL, TEAM_COLL_IS_PARENT_COLL,
TEAM_COL_SAME_NEXT_COLL, TEAM_COL_SAME_NEXT_COLL,
TEAM_COL_REORDERING_FAILED, TEAM_COL_REORDERING_FAILED,
TEAM_COLL_DATA_INVALID,
} from '../errors'; } from '../errors';
import { PubSubService } from '../pubsub/pubsub.service'; import { PubSubService } from '../pubsub/pubsub.service';
import { isValidLength } from 'src/utils'; import { isValidLength } from 'src/utils';
@@ -70,7 +69,6 @@ export class TeamCollectionService {
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1), this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
), ),
}, },
data: folder.data ?? undefined,
}; };
} }
@@ -120,7 +118,6 @@ export class TeamCollectionService {
name: collection.right.title, name: collection.right.title,
folders: childrenCollectionObjects, folders: childrenCollectionObjects,
requests: requests.map((x) => x.request), requests: requests.map((x) => x.request),
data: JSON.stringify(collection.right.data),
}; };
return E.right(result); return E.right(result);
@@ -201,11 +198,8 @@ export class TeamCollectionService {
), ),
); );
teamCollections.forEach((collection) => teamCollections.forEach((x) =>
this.pubsub.publish( this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
`team_coll/${destTeamID}/coll_added`,
this.cast(collection),
),
); );
return E.right(true); return E.right(true);
@@ -274,11 +268,8 @@ export class TeamCollectionService {
), ),
); );
teamCollections.forEach((collections) => teamCollections.forEach((x) =>
this.pubsub.publish( this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
`team_coll/${destTeamID}/coll_added`,
this.cast(collections),
),
); );
return E.right(true); return E.right(true);
@@ -286,17 +277,11 @@ export class TeamCollectionService {
/** /**
* Typecast a database TeamCollection to a TeamCollection model * Typecast a database TeamCollection to a TeamCollection model
*
* @param teamCollection database TeamCollection * @param teamCollection database TeamCollection
* @returns TeamCollection model * @returns TeamCollection model
*/ */
private cast(teamCollection: DBTeamCollection): TeamCollection { private cast(teamCollection: DBTeamCollection): TeamCollection {
return <TeamCollection>{ return <TeamCollection>{ ...teamCollection };
id: teamCollection.id,
title: teamCollection.title,
parentID: teamCollection.parentID,
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
};
} }
/** /**
@@ -339,7 +324,7 @@ export class TeamCollectionService {
}); });
if (!teamCollection) return null; if (!teamCollection) return null;
return !teamCollection.parent ? null : this.cast(teamCollection.parent); return teamCollection.parent;
} }
/** /**
@@ -350,12 +335,12 @@ export class TeamCollectionService {
* @param take Number of items we want returned * @param take Number of items we want returned
* @returns A list of child collections * @returns A list of child collections
*/ */
async getChildrenOfCollection( getChildrenOfCollection(
collectionID: string, collectionID: string,
cursor: string | null, cursor: string | null,
take: number, take: number,
) { ) {
const res = await this.prisma.teamCollection.findMany({ return this.prisma.teamCollection.findMany({
where: { where: {
parentID: collectionID, parentID: collectionID,
}, },
@@ -366,12 +351,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const childCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return childCollections;
} }
/** /**
@@ -387,7 +366,7 @@ export class TeamCollectionService {
cursor: string | null, cursor: string | null,
take: number, take: number,
) { ) {
const res = await this.prisma.teamCollection.findMany({ return this.prisma.teamCollection.findMany({
where: { where: {
teamID, teamID,
parentID: null, parentID: null,
@@ -399,12 +378,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const teamCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return teamCollections;
} }
/** /**
@@ -497,7 +470,6 @@ export class TeamCollectionService {
async createCollection( async createCollection(
teamID: string, teamID: string,
title: string, title: string,
data: string | null = null,
parentTeamCollectionID: string | null, parentTeamCollectionID: string | null,
) { ) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH); const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
@@ -509,13 +481,6 @@ export class TeamCollectionService {
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER); if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
} }
if (data === '') return E.left(TEAM_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
data = jsonReq.right;
}
const isParent = parentTeamCollectionID const isParent = parentTeamCollectionID
? { ? {
connect: { connect: {
@@ -533,23 +498,18 @@ export class TeamCollectionService {
}, },
}, },
parent: isParent, parent: isParent,
data: data ?? undefined,
orderIndex: !parentTeamCollectionID orderIndex: !parentTeamCollectionID
? (await this.getRootCollectionsCount(teamID)) + 1 ? (await this.getRootCollectionsCount(teamID)) + 1
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1, : (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
}, },
}); });
this.pubsub.publish( this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection);
`team_coll/${teamID}/coll_added`,
this.cast(teamCollection),
);
return E.right(this.cast(teamCollection)); return E.right(this.cast(teamCollection));
} }
/** /**
* @deprecated Use updateTeamCollection method instead
* Update the title of a TeamCollection * Update the title of a TeamCollection
* *
* @param collectionID The Collection ID * @param collectionID The Collection ID
@@ -572,10 +532,10 @@ export class TeamCollectionService {
this.pubsub.publish( this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`, `team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection), updatedTeamCollection,
); );
return E.right(this.cast(updatedTeamCollection)); return E.right(updatedTeamCollection);
} catch (error) { } catch (error) {
return E.left(TEAM_COLL_NOT_FOUND); return E.left(TEAM_COLL_NOT_FOUND);
} }
@@ -734,8 +694,8 @@ export class TeamCollectionService {
* @returns An Option of boolean, is parent or not * @returns An Option of boolean, is parent or not
*/ */
private async isParent( private async isParent(
collection: DBTeamCollection, collection: TeamCollection,
destCollection: DBTeamCollection, destCollection: TeamCollection,
): Promise<O.Option<boolean>> { ): Promise<O.Option<boolean>> {
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null //* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
//* Valid condition, isParent returns false //* Valid condition, isParent returns false
@@ -1011,49 +971,4 @@ export class TeamCollectionService {
const teamCollectionsCount = this.prisma.teamCollection.count(); const teamCollectionsCount = this.prisma.teamCollection.count();
return teamCollectionsCount; return teamCollectionsCount;
} }
/**
* Update Team Collection details
*
* @param collectionID Collection ID
* @param collectionData new header data in a JSONified string form
* @param newTitle New title of the collection
* @returns Updated TeamCollection
*/
async updateTeamCollection(
collectionID: string,
collectionData: string = null,
newTitle: string = null,
) {
try {
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE);
}
if (collectionData === '') return E.left(TEAM_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
const updatedTeamCollection = await this.prisma.teamCollection.update({
where: { id: collectionID },
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection),
);
return E.right(this.cast(updatedTeamCollection));
} catch (e) {
return E.left(TEAM_COLL_NOT_FOUND);
}
}
} }

View File

@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaModule } from 'src/prisma/prisma.module';
import { PubSubModule } from 'src/pubsub/pubsub.module'; import { PubSubModule } from 'src/pubsub/pubsub.module';
import { TeamModule } from 'src/team/team.module'; import { TeamModule } from 'src/team/team.module';
@@ -11,7 +12,7 @@ import { TeamInviteeGuard } from './team-invitee.guard';
import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver'; import { TeamTeamInviteExtResolver } from './team-teaminvite-ext.resolver';
@Module({ @Module({
imports: [PrismaModule, TeamModule, PubSubModule, UserModule], imports: [PrismaModule, TeamModule, PubSubModule, UserModule, MailerModule],
providers: [ providers: [
TeamInvitationService, TeamInvitationService,
TeamInvitationResolver, TeamInvitationResolver,

View File

@@ -20,7 +20,6 @@ import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils'; import { validateEmail } from '../utils';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class TeamInvitationService { export class TeamInvitationService {
@@ -29,8 +28,8 @@ export class TeamInvitationService {
private readonly userService: UserService, private readonly userService: UserService,
private readonly teamService: TeamService, private readonly teamService: TeamService,
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
private readonly configService: ConfigService,
) {} ) {}
/** /**
@@ -151,9 +150,7 @@ export class TeamInvitationService {
template: 'team-invitation', template: 'team-invitation',
variables: { variables: {
invitee: creator.displayName ?? 'A Hoppscotch User', invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${ action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
dbInvitation.id
}`,
invite_team_name: team.name, invite_team_name: team.name,
}, },
}); });

View File

@@ -42,7 +42,6 @@ const teamCollection: DbTeamCollection = {
id: 'team-coll-1', id: 'team-coll-1',
parentID: null, parentID: null,
teamID: team.id, teamID: team.id,
data: {},
title: 'Team Collection 1', title: 'Team Collection 1',
orderIndex: 1, orderIndex: 1,
createdOn: new Date(), createdOn: new Date(),

View File

@@ -1,8 +1,6 @@
// This interface defines how data will be received from the app when we are importing Hoppscotch collections
export interface CollectionFolder { export interface CollectionFolder {
id?: string; id?: string;
folders: CollectionFolder[]; folders: CollectionFolder[];
requests: any[]; requests: any[];
name: string; name: string;
data?: string;
} }

View File

@@ -1,29 +0,0 @@
export enum InfraConfigEnum {
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
}
export enum InfraConfigEnumForClient {
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
}

View File

@@ -6,13 +6,6 @@ import { PaginationArgs } from 'src/types/input-types.args';
export class CreateRootUserCollectionArgs { export class CreateRootUserCollectionArgs {
@Field({ name: 'title', description: 'Title of the new user collection' }) @Field({ name: 'title', description: 'Title of the new user collection' })
title: string; title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
export class CreateChildUserCollectionArgs { export class CreateChildUserCollectionArgs {
@@ -24,13 +17,6 @@ export class CreateChildUserCollectionArgs {
description: 'ID of the parent to the new user collection', description: 'ID of the parent to the new user collection',
}) })
parentUserCollectionID: string; parentUserCollectionID: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
@@ -109,26 +95,3 @@ export class ImportUserCollectionsFromJSONArgs {
}) })
parentCollectionID?: string; parentCollectionID?: string;
} }
@ArgsType()
export class UpdateUserCollectionsArgs {
@Field(() => ID, {
name: 'userCollectionID',
description: 'ID of the user collection',
})
userCollectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the user collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -30,7 +30,6 @@ import {
MoveUserCollectionArgs, MoveUserCollectionArgs,
RenameUserCollectionsArgs, RenameUserCollectionsArgs,
UpdateUserCollectionArgs, UpdateUserCollectionArgs,
UpdateUserCollectionsArgs,
} from './input-type.args'; } from './input-type.args';
import { ReqType } from 'src/types/RequestTypes'; import { ReqType } from 'src/types/RequestTypes';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
@@ -143,13 +142,7 @@ export class UserCollectionResolver {
); );
if (E.isLeft(userCollection)) throwErr(userCollection.left); if (E.isLeft(userCollection)) throwErr(userCollection.left);
return <UserCollection>{ return userCollection.right;
...userCollection.right,
userID: userCollection.right.userUid,
data: !userCollection.right.data
? null
: JSON.stringify(userCollection.right.data),
};
} }
@Query(() => UserCollectionExportJSONData, { @Query(() => UserCollectionExportJSONData, {
@@ -198,7 +191,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
null, null,
ReqType.REST, ReqType.REST,
); );
@@ -220,7 +212,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
null, null,
ReqType.GQL, ReqType.GQL,
); );
@@ -241,7 +232,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
args.parentUserCollectionID, args.parentUserCollectionID,
ReqType.GQL, ReqType.GQL,
); );
@@ -262,7 +252,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
args.parentUserCollectionID, args.parentUserCollectionID,
ReqType.REST, ReqType.REST,
); );
@@ -370,26 +359,6 @@ export class UserCollectionResolver {
return importedCollection.right; return importedCollection.right;
} }
@Mutation(() => UserCollection, {
description: 'Update a UserCollection',
})
@UseGuards(GqlAuthGuard)
async updateUserCollection(
@GqlUser() user: AuthUser,
@Args() args: UpdateUserCollectionsArgs,
) {
const updatedUserCollection =
await this.userCollectionService.updateUserCollection(
args.newTitle,
args.data,
args.userCollectionID,
user.uid,
);
if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left);
return updatedUserCollection.right;
}
// Subscriptions // Subscriptions
@Subscription(() => UserCollection, { @Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation', description: 'Listen for User Collection Creation',

View File

@@ -12,7 +12,6 @@ import {
USER_NOT_FOUND, USER_NOT_FOUND,
USER_NOT_OWNER, USER_NOT_OWNER,
USER_COLL_INVALID_JSON, USER_COLL_INVALID_JSON,
USER_COLL_DATA_INVALID,
} from 'src/errors'; } from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
@@ -44,12 +43,8 @@ export class UserCollectionService {
*/ */
private cast(collection: UserCollection) { private cast(collection: UserCollection) {
return <UserCollectionModel>{ return <UserCollectionModel>{
id: collection.id, ...collection,
title: collection.title,
type: collection.type,
parentID: collection.parentID,
userID: collection.userUid, userID: collection.userUid,
data: !collection.data ? null : JSON.stringify(collection.data),
}; };
} }
@@ -151,7 +146,7 @@ export class UserCollectionService {
}, },
}); });
return !parent ? null : this.cast(parent); return parent;
} }
/** /**
@@ -169,7 +164,7 @@ export class UserCollectionService {
take: number, take: number,
type: ReqType, type: ReqType,
) { ) {
const res = await this.prisma.userCollection.findMany({ return this.prisma.userCollection.findMany({
where: { where: {
parentID: collectionID, parentID: collectionID,
type: type, type: type,
@@ -181,12 +176,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
} }
/** /**
@@ -222,20 +211,12 @@ export class UserCollectionService {
async createUserCollection( async createUserCollection(
user: AuthUser, user: AuthUser,
title: string, title: string,
data: string | null = null,
parentUserCollectionID: string | null, parentUserCollectionID: string | null,
type: ReqType, type: ReqType,
) { ) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH); const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE); if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
if (data === '') return E.left(USER_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
data = jsonReq.right;
}
// If creating a child collection // If creating a child collection
if (parentUserCollectionID !== null) { if (parentUserCollectionID !== null) {
const parentCollection = await this.getUserCollection( const parentCollection = await this.getUserCollection(
@@ -270,19 +251,15 @@ export class UserCollectionService {
}, },
}, },
parent: isParent, parent: isParent,
data: data ?? undefined,
orderIndex: !parentUserCollectionID orderIndex: !parentUserCollectionID
? (await this.getRootCollectionsCount(user.uid)) + 1 ? (await this.getRootCollectionsCount(user.uid)) + 1
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1, : (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
}, },
}); });
await this.pubsub.publish( await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
`user_coll/${user.uid}/created`,
this.cast(userCollection),
);
return E.right(this.cast(userCollection)); return E.right(userCollection);
} }
/** /**
@@ -299,7 +276,7 @@ export class UserCollectionService {
take: number, take: number,
type: ReqType, type: ReqType,
) { ) {
const res = await this.prisma.userCollection.findMany({ return this.prisma.userCollection.findMany({
where: { where: {
userUid: user.uid, userUid: user.uid,
parentID: null, parentID: null,
@@ -312,12 +289,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const userCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return userCollections;
} }
/** /**
@@ -336,7 +307,7 @@ export class UserCollectionService {
take: number, take: number,
type: ReqType, type: ReqType,
) { ) {
const res = await this.prisma.userCollection.findMany({ return this.prisma.userCollection.findMany({
where: { where: {
userUid: user.uid, userUid: user.uid,
parentID: userCollectionID, parentID: userCollectionID,
@@ -346,16 +317,9 @@ export class UserCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
} }
/** /**
* @deprecated Use updateUserCollection method instead
* Update the title of a UserCollection * Update the title of a UserCollection
* *
* @param newTitle The new title of collection * @param newTitle The new title of collection
@@ -387,10 +351,10 @@ export class UserCollectionService {
this.pubsub.publish( this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`, `user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection), updatedUserCollection,
); );
return E.right(this.cast(updatedUserCollection)); return E.right(updatedUserCollection);
} catch (error) { } catch (error) {
return E.left(USER_COLL_NOT_FOUND); return E.left(USER_COLL_NOT_FOUND);
} }
@@ -627,10 +591,10 @@ export class UserCollectionService {
this.pubsub.publish( this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`, `user_coll/${collection.right.userUid}/moved`,
this.cast(updatedCollection.right), updatedCollection.right,
); );
return E.right(this.cast(updatedCollection.right)); return E.right(updatedCollection.right);
} }
// destCollectionID != null i.e move into another collection // destCollectionID != null i.e move into another collection
@@ -678,10 +642,10 @@ export class UserCollectionService {
this.pubsub.publish( this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`, `user_coll/${collection.right.userUid}/moved`,
this.cast(updatedCollection.right), updatedCollection.right,
); );
return E.right(this.cast(updatedCollection.right)); return E.right(updatedCollection.right);
} }
/** /**
@@ -882,7 +846,6 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread ...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
}; };
}), }),
data: JSON.stringify(collection.right.data),
}; };
return E.right(result); return E.right(result);
@@ -955,7 +918,6 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread ...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
}; };
}), }),
data: JSON.stringify(parentCollection.right.data),
}), }),
collectionType: parentCollection.right.type, collectionType: parentCollection.right.type,
}); });
@@ -1009,7 +971,6 @@ export class UserCollectionService {
this.generatePrismaQueryObj(f, userID, index + 1, reqType), this.generatePrismaQueryObj(f, userID, index + 1, reqType),
), ),
}, },
data: folder.data ?? undefined,
}; };
} }
@@ -1079,63 +1040,10 @@ export class UserCollectionService {
), ),
); );
userCollections.forEach((collection) => userCollections.forEach((x) =>
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)), this.pubsub.publish(`user_coll/${userID}/created`, x),
); );
return E.right(true); return E.right(true);
} }
/**
* Update a UserCollection
*
* @param newTitle The new title of collection
* @param userCollectionID The Collection Id
* @param userID The User UID
* @returns An Either of the updated UserCollection
*/
async updateUserCollection(
newTitle: string = null,
collectionData: string | null = null,
userCollectionID: string,
userID: string,
) {
if (collectionData === '') return E.left(USER_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
}
// Check to see is the collection belongs to the user
const isOwner = await this.isOwnerCheck(userCollectionID, userID);
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
try {
const updatedUserCollection = await this.prisma.userCollection.update({
where: {
id: userCollectionID,
},
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection),
);
return E.right(this.cast(updatedUserCollection));
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
} }

View File

@@ -13,12 +13,6 @@ export class UserCollection {
}) })
title: string; title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ReqType, { @Field(() => ReqType, {
description: 'Type of the user collection', description: 'Type of the user collection',
}) })

View File

@@ -131,26 +131,6 @@ export const validateEmail = (email: string) => {
).test(email); ).test(email);
}; };
/**
* Checks to see if the URL is valid or not
* @param url The URL to validate
* @returns boolean
*/
export const validateSMTPUrl = (url: string) => {
// Possible valid formats
// smtp(s)://mail.example.com
// smtp(s)://user:pass@mail.example.com
// smtp(s)://mail.example.com:587
// smtp(s)://user:pass@mail.example.com:587
if (!url || url.length === 0) return false;
const regex =
/^(smtp|smtps):\/\/(?:([^:]+):([^@]+)@)?((?!\.)[^:]+)(?::(\d+))?$/;
if (regex.test(url)) return true;
return false;
};
/** /**
* String to JSON parser * String to JSON parser
* @param {str} str The string to parse * @param {str} str The string to parse
@@ -181,23 +161,21 @@ export function isValidLength(title: string, length: number) {
/** /**
* This function is called by bootstrap() in main.ts * This function is called by bootstrap() in main.ts
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not. * It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error. * If not, it throws an error.
*/ */
export function checkEnvironmentAuthProvider( export function checkEnvironmentAuthProvider() {
VITE_ALLOWED_AUTH_PROVIDERS: string, if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
) {
if (!VITE_ALLOWED_AUTH_PROVIDERS) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS); throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
} }
if (VITE_ALLOWED_AUTH_PROVIDERS === '') { if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS); throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
} }
const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map( const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
(provider) => provider.toLocaleUpperCase(), ',',
); ).map((provider) => provider.toLocaleUpperCase());
const supportedAuthProviders = Object.values(AuthProvider).map( const supportedAuthProviders = Object.values(AuthProvider).map(
(provider: string) => provider.toLocaleUpperCase(), (provider: string) => provider.toLocaleUpperCase(),
); );

View File

@@ -147,7 +147,7 @@ module.exports = {
// The glob patterns Jest uses to detect test files // The glob patterns Jest uses to detect test files
testMatch: [ testMatch: [
// "**/__tests__/**/*.[jt]s?(x)", // "**/__tests__/**/*.[jt]s?(x)",
"**/src/__tests__/commands/**/*.*.ts", "**/src/__tests__/**/*.*.ts",
], ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped

View File

@@ -19,9 +19,8 @@
"debugger": "node debugger.js 9999", "debugger": "node debugger.js 9999",
"prepublish": "pnpm exec tsup", "prepublish": "pnpm exec tsup",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "pnpm run build && jest && rm -rf dist",
"do-typecheck": "pnpm exec tsc --noEmit", "do-typecheck": "pnpm exec tsc --noEmit",
"do-test": "pnpm test" "test": "pnpm run build && jest && rm -rf dist"
}, },
"keywords": [ "keywords": [
"cli", "cli",

View File

@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR"); expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
}); });
test("Malformed collection file.", async () => { test("Malformed collection file.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath( const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json" "malformed-collection2.json"
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json"); const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json"); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`; const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await execAsync(cmd); const { error } = await execAsync(cmd);
expect(error).toBeNull(); expect(error).toBeNull();
}); });
@@ -129,6 +129,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`; const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stderr } = await execAsync(cmd); const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr); const out = getErrorCode(stderr);
console.log("invalid value thing", out)
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT"); expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
}); });

View File

@@ -1,6 +1,7 @@
{ {
"URL": "https://echo.hoppscotch.io", "URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io", "HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value", "BODY_VALUE": "body_value",
"BODY_KEY": "body_key" "BODY_KEY": "body_key"
} }

View File

@@ -12,7 +12,7 @@
"method": "POST", "method": "POST",
"auth": { "authType": "none", "authActive": true }, "auth": { "authType": "none", "authActive": true },
"preRequestScript": "", "preRequestScript": "",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})", "testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"body": { "body": {
"contentType": "application/json", "contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}" "body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"

View File

@@ -6,24 +6,23 @@ import {
parseTemplateString, parseTemplateString,
parseTemplateStringE, parseTemplateStringE,
} from "@hoppscotch/data"; } from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node"; import { runPreRequestScript } from "@hoppscotch/js-sandbox";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import * as RA from "fp-ts/ReadonlyArray";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function"; import { flow, pipe } from "fp-ts/function";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as O from "fp-ts/Option";
import * as S from "fp-ts/string"; import * as S from "fp-ts/string";
import qs from "qs"; import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request"; import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { HoppCLIError, error } from "../types/errors"; import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request"; import { HoppEnvs } from "../types/request";
import { PreRequestMetrics } from "../types/response";
import { isHoppCLIError } from "./checks"; import { isHoppCLIError } from "./checks";
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array"; import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
import { getEffectiveFinalMetaData } from "./getters";
import { toFormData } from "./mutators"; import { toFormData } from "./mutators";
import { getEffectiveFinalMetaData } from "./getters";
import { PreRequestMetrics } from "../types/response";
/** /**
* Runs pre-request-script runner over given request which extracts set ENVs and * Runs pre-request-script runner over given request which extracts set ENVs and

View File

@@ -1,19 +1,17 @@
import { HoppRESTRequest } from "@hoppscotch/data"; import { HoppRESTRequest } from "@hoppscotch/data";
import { TestDescriptor } from "@hoppscotch/js-sandbox"; import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
import { runTestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as RA from "fp-ts/ReadonlyArray";
import * as T from "fp-ts/Task";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import { hrtime } from "process"; import { hrtime } from "process";
import { flow, pipe } from "fp-ts/function";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as TE from "fp-ts/TaskEither";
import * as T from "fp-ts/Task";
import { import {
RequestRunnerResponse, RequestRunnerResponse,
TestReport, TestReport,
TestScriptParams, TestScriptParams,
} from "../interfaces/response"; } from "../interfaces/response";
import { HoppCLIError, error } from "../types/errors"; import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request"; import { HoppEnvs } from "../types/request";
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response"; import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
import { getDurationInSeconds } from "./getters"; import { getDurationInSeconds } from "./getters";
@@ -38,7 +36,7 @@ export const testRunner = (
pipe( pipe(
TE.of(testScriptData), TE.of(testScriptData),
TE.chain(({ testScript, response, envs }) => TE.chain(({ testScript, response, envs }) =>
runTestScript(testScript, envs, response) execTestScript(testScript, envs, response)
) )
) )
), ),

View File

@@ -69,7 +69,5 @@ module.exports = {
"Do not use 'localStorage' directly. Please use the PersistenceService", "Do not use 'localStorage' directly. Please use the PersistenceService",
}, },
], ],
eqeqeq: 1,
"no-else-return": 1,
}, },
} }

View File

@@ -5,4 +5,5 @@ module.exports = {
printWidth: 80, printWidth: 80,
useTabs: false, useTabs: false,
tabWidth: 2, tabWidth: 2,
plugins: ["prettier-plugin-tailwindcss"],
} }

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>

Before

Width:  |  Height:  |  Size: 1017 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 684 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 633 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>

Before

Width:  |  Height:  |  Size: 610 B

View File

@@ -29,6 +29,14 @@
@apply antialiased; @apply antialiased;
accent-color: var(--accent-color); accent-color: var(--accent-color);
font-variant-ligatures: common-ligatures; font-variant-ligatures: common-ligatures;
// Colors
--info-color: #ec4899;
--success-color: #10b981;
--blue-color: #3b82f6;
--warning-color: #f59e0b;
--cl-error-color: #ef4444;
--sv-error-color: #dc2626;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@@ -57,7 +65,7 @@ input::placeholder,
textarea::placeholder, textarea::placeholder,
.cm-placeholder { .cm-placeholder {
@apply text-secondary; @apply text-secondary;
@apply opacity-50 #{!important}; @apply opacity-50;
} }
input, input,
@@ -76,7 +84,7 @@ body {
@apply font-medium; @apply font-medium;
@apply select-none; @apply select-none;
@apply overflow-x-hidden; @apply overflow-x-hidden;
@apply leading-body #{!important}; @apply leading-body;
animation: fade 300ms forwards; animation: fade 300ms forwards;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none; -webkit-touch-callout: none;
@@ -174,7 +182,7 @@ a {
@apply font-semibold; @apply font-semibold;
@apply px-2 py-1; @apply px-2 py-1;
@apply truncate; @apply truncate;
@apply leading-body; @apply leading-normal;
@apply items-center; @apply items-center;
kbd { kbd {
@@ -221,7 +229,7 @@ a {
@apply overflow-y-auto; @apply overflow-y-auto;
@apply text-body text-secondary; @apply text-body text-secondary;
@apply p-2; @apply p-2;
@apply leading-body; @apply leading-normal;
@apply focus:outline-none; @apply focus:outline-none;
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -253,7 +261,7 @@ a {
hr { hr {
@apply border-b border-dividerLight; @apply border-b border-dividerLight;
@apply my-2 #{!important}; @apply my-2;
} }
.heading { .heading {
@@ -342,28 +350,44 @@ pre.ace_editor {
} }
} }
.select-wrapper {
@apply flex flex-1;
@apply relative;
@apply after:absolute;
@apply after:flex;
@apply after:inset-y-0;
@apply after:items-center;
@apply after:justify-center;
@apply after:pointer-events-none;
@apply after:font-icon;
@apply after:text-current;
@apply after:right-3;
@apply after:content-["\e5cf"];
@apply after:text-lg;
}
.info-response { .info-response {
color: var(--status-info-color); color: var(--info-color);
} }
.success-response { .success-response {
color: var(--status-success-color); color: var(--success-color);
} }
.redirect-response { .redir-response {
color: var(--status-redirect-color); color: var(--warning-color);
} }
.critical-error-response { .cl-error-response {
color: var(--status-critical-error-color); color: var(--cl-error-color);
} }
.server-error-response { .sv-error-response {
color: var(--status-server-error-color); color: var(--sv-error-color);
} }
.missing-data-response { .missing-data-response {
color: var(--status-missing-data-color); @apply text-secondaryLight;
} }
.toasted-container { .toasted-container {
@@ -513,12 +537,12 @@ pre.ace_editor {
@apply inline-flex; @apply inline-flex;
@apply font-sans; @apply font-sans;
@apply text-tiny; @apply text-tiny;
@apply bg-dividerLight; @apply bg-divider;
@apply rounded; @apply rounded;
@apply ml-2; @apply ml-2;
@apply px-1; @apply px-1;
@apply min-w-[1.25rem]; @apply min-w-5;
@apply min-h-[1.25rem]; @apply min-h-5;
@apply items-center; @apply items-center;
@apply justify-center; @apply justify-center;
@apply border border-dividerDark; @apply border border-dividerDark;

View File

@@ -1,89 +1,89 @@
@mixin green-theme { @mixin green-theme {
--accent-color: theme("colors.emerald.500"); --accent-color: #10b981;
--accent-light-color: theme("colors.emerald.400"); --accent-light-color: #34d399;
--accent-dark-color: theme("colors.emerald.600"); --accent-dark-color: #059669;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.emerald.400"); --gradient-from-color: #a7f3d0;
--gradient-via-color: theme("colors.emerald.500"); --gradient-via-color: #34d399;
--gradient-to-color: theme("colors.emerald.600"); --gradient-to-color: #059669;
} }
@mixin teal-theme { @mixin teal-theme {
--accent-color: theme("colors.teal.500"); --accent-color: #14b8a6;
--accent-light-color: theme("colors.teal.400"); --accent-light-color: #2dd4bf;
--accent-dark-color: theme("colors.teal.600"); --accent-dark-color: #0d9488;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.teal.400"); --gradient-from-color: #99f6e4;
--gradient-via-color: theme("colors.teal.500"); --gradient-via-color: #2dd4bf;
--gradient-to-color: theme("colors.teal.600"); --gradient-to-color: #0d9488;
} }
@mixin blue-theme { @mixin blue-theme {
--accent-color: theme("colors.blue.500"); --accent-color: #3b82f6;
--accent-light-color: theme("colors.blue.400"); --accent-light-color: #60a5fa;
--accent-dark-color: theme("colors.blue.600"); --accent-dark-color: #2563eb;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.blue.400"); --gradient-from-color: #bfdbfe;
--gradient-via-color: theme("colors.blue.500"); --gradient-via-color: #60a5fa;
--gradient-to-color: theme("colors.blue.600"); --gradient-to-color: #2563eb;
} }
@mixin indigo-theme { @mixin indigo-theme {
--accent-color: theme("colors.indigo.500"); --accent-color: #6366f1;
--accent-light-color: theme("colors.indigo.400"); --accent-light-color: #818cf8;
--accent-dark-color: theme("colors.indigo.600"); --accent-dark-color: #4f46e5;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.indigo.400"); --gradient-from-color: #c7d2fe;
--gradient-via-color: theme("colors.indigo.500"); --gradient-via-color: #818cf8;
--gradient-to-color: theme("colors.indigo.600"); --gradient-to-color: #4f46e5;
} }
@mixin purple-theme { @mixin purple-theme {
--accent-color: theme("colors.purple.500"); --accent-color: #8b5cf6;
--accent-light-color: theme("colors.purple.400"); --accent-light-color: #a78bfa;
--accent-dark-color: theme("colors.purple.600"); --accent-dark-color: #7c3aed;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.purple.400"); --gradient-from-color: #ddd6fe;
--gradient-via-color: theme("colors.purple.500"); --gradient-via-color: #a78bfa;
--gradient-to-color: theme("colors.purple.600"); --gradient-to-color: #7c3aed;
} }
@mixin yellow-theme { @mixin yellow-theme {
--accent-color: theme("colors.amber.500"); --accent-color: #f59e0b;
--accent-light-color: theme("colors.amber.400"); --accent-light-color: #fbbf24;
--accent-dark-color: theme("colors.amber.600"); --accent-dark-color: #d97706;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.amber.400"); --gradient-from-color: #fde68a;
--gradient-via-color: theme("colors.amber.500"); --gradient-via-color: #fbbf24;
--gradient-to-color: theme("colors.amber.600"); --gradient-to-color: #d97706;
} }
@mixin orange-theme { @mixin orange-theme {
--accent-color: theme("colors.orange.500"); --accent-color: #f97316;
--accent-light-color: theme("colors.orange.400"); --accent-light-color: #fb923c;
--accent-dark-color: theme("colors.orange.600"); --accent-dark-color: #ea580c;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.orange.400"); --gradient-from-color: #fed7aa;
--gradient-via-color: theme("colors.orange.500"); --gradient-via-color: #fb923c;
--gradient-to-color: theme("colors.orange.600"); --gradient-to-color: #ea580c;
} }
@mixin red-theme { @mixin red-theme {
--accent-color: theme("colors.red.500"); --accent-color: #ef4444;
--accent-light-color: theme("colors.red.400"); --accent-light-color: #f87171;
--accent-dark-color: theme("colors.red.600"); --accent-dark-color: #dc2626;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.red.400"); --gradient-from-color: #fecaca;
--gradient-via-color: theme("colors.red.500"); --gradient-via-color: #f87171;
--gradient-to-color: theme("colors.red.600"); --gradient-to-color: #dc2626;
} }
@mixin pink-theme { @mixin pink-theme {
--accent-color: theme("colors.pink.500"); --accent-color: #ec4899;
--accent-light-color: theme("colors.pink.400"); --accent-light-color: #f472b6;
--accent-dark-color: theme("colors.pink.600"); --accent-dark-color: #db2777;
--accent-contrast-color: theme("colors.white"); --accent-contrast-color: #fff;
--gradient-from-color: theme("colors.pink.400"); --gradient-from-color: #fbcfe8;
--gradient-via-color: theme("colors.pink.500"); --gradient-via-color: #f472b6;
--gradient-to-color: theme("colors.pink.600"); --gradient-to-color: #db2777;
} }

View File

@@ -1,16 +1,17 @@
@mixin base-theme { @mixin base-theme {
--font-sans: "Inter Variable", sans-serif; --font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace; --font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem; --font-size-body: 0.75rem;
--font-size-tiny: 0.625rem; --font-size-tiny: 0.688rem;
--line-height-body: 1rem; --line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem; --upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem; --upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem; --upper-tertiary-sticky-fold: 8.25rem;
--upper-fourth-sticky-fold: 10.2rem; --upper-fourth-sticky-fold: 10.2rem;
--upper-mobile-primary-sticky-fold: 6.75rem; --upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.813rem; --upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.875rem; --upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem; --upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem; --lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem; --lower-secondary-sticky-fold: 5.063rem;
@@ -19,122 +20,62 @@
--sidebar-primary-sticky-fold: 2rem; --sidebar-primary-sticky-fold: 2rem;
} }
@mixin light-theme {
--primary-color: theme("colors.white");
--primary-light-color: theme("colors.gray.50");
--primary-dark-color: theme("colors.gray.100");
--primary-contrast-color: #fdfdfd;
--secondary-color: theme("colors.gray.500");
--secondary-light-color: theme("colors.gray.400");
--secondary-dark-color: theme("colors.gray.900");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.gray.100");
--divider-dark-color: theme("colors.gray.300");
--banner-info-color: theme("colors.stone.100");
--banner-warning-color: theme("colors.yellow.100");
--banner-error-color: theme("colors.red.100");
--tooltip-color: theme("colors.neutral.800");
--popover-color: theme("colors.white");
--method-get-color: theme("colors.green.500");
--method-post-color: theme("colors.amber.500");
--method-put-color: theme("colors.blue.500");
--method-patch-color: theme("colors.purple.500");
--method-delete-color: theme("colors.red.500");
--method-head-color: theme("colors.lime.500");
--method-options-color: theme("colors.pink.500");
--method-default-color: theme("colors.gray.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "textmate";
}
@mixin dark-theme { @mixin dark-theme {
--primary-color: #181818; --primary-color: #181818;
--primary-light-color: #1c1c1e; --primary-light-color: #1c1c1e;
--primary-dark-color: theme("colors.neutral.800"); --primary-dark-color: #262626;
--primary-contrast-color: theme("colors.neutral.900"); --primary-contrast-color: #171717;
--secondary-color: theme("colors.neutral.400"); --secondary-color: #a3a3a3;
--secondary-light-color: theme("colors.neutral.500"); --secondary-light-color: #737373;
--secondary-dark-color: theme("colors.zinc.50"); --secondary-dark-color: #fafafa;
--divider-color: #1f1f1f; --divider-color: #262626;
--divider-light-color: #1f1f1f; --divider-light-color: #1f1f1f;
--divider-dark-color: theme("colors.zinc.800"); --divider-dark-color: #2d2d2d;
--banner-info-color: theme("colors.stone.800"); --error-color: #292524;
--banner-warning-color: theme("colors.yellow.800"); --tooltip-color: #f5f5f5;
--banner-error-color: theme("colors.red.800");
--tooltip-color: theme("colors.neutral.100");
--popover-color: #1b1b1b; --popover-color: #1b1b1b;
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.neutral.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "merbivore_soft"; --editor-theme: "merbivore_soft";
} }
@mixin light-theme {
--primary-color: #ffffff;
--primary-light-color: #f9fafb;
--primary-dark-color: #f3f4f6;
--primary-contrast-color: #fdfdfd;
--secondary-color: #6b7280;
--secondary-light-color: #9ca3af;
--secondary-dark-color: #111827;
--divider-color: #f3f4f6;
--divider-light-color: #f3f4f6;
--divider-dark-color: #d1d5db;
--error-color: #fef3c7;
--tooltip-color: #262626;
--popover-color: #ffffff;
--editor-theme: "textmate";
}
@mixin black-theme { @mixin black-theme {
--primary-color: #0f0f0f; --primary-color: #0f0f0f;
--primary-light-color: theme("colors.neutral.900"); --primary-light-color: #171717;
--primary-dark-color: #181818; --primary-dark-color: #181818;
--primary-contrast-color: #0f0f0f; --primary-contrast-color: #0f0f0f;
--secondary-color: theme("colors.neutral.400"); --secondary-color: #a3a3a3;
--secondary-light-color: theme("colors.neutral.500"); --secondary-light-color: #737373;
--secondary-dark-color: theme("colors.neutral.50"); --secondary-dark-color: #f5f5f5;
--divider-color: theme("colors.neutral.900"); --divider-color: #1c1c1e;
--divider-light-color: theme("colors.neutral.900"); --divider-light-color: #181818;
--divider-dark-color: theme("colors.zinc.800"); --divider-dark-color: #323232;
--banner-info-color: theme("colors.stone.900");
--banner-warning-color: theme("colors.yellow.900");
--banner-error-color: theme("colors.red.900");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.stone.950");
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.zinc.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--error-color: #1c1917;
--tooltip-color: #f5f5f5;
--popover-color: #0f0f0f;
--editor-theme: "twilight"; --editor-theme: "twilight";
} }

View File

@@ -1,41 +1,41 @@
@mixin light-editor-theme { @mixin dark-editor-theme {
--editor-type-color: theme("colors.violet.600"); --editor-type-color: #a78bfa;
--editor-name-color: theme("colors.red.600"); --editor-name-color: #60a5fa;
--editor-operator-color: theme("colors.indigo.600"); --editor-operator-color: #818cf8;
--editor-invalid-color: theme("colors.red.600"); --editor-invalid-color: #f87171;
--editor-separator-color: theme("colors.gray.600"); --editor-separator-color: #9ca3af;
--editor-meta-color: theme("colors.gray.600"); --editor-meta-color: #9ca3af;
--editor-variable-color: theme("colors.emerald.600"); --editor-variable-color: #34d399;
--editor-link-color: theme("colors.cyan.600"); --editor-link-color: #22d3ee;
--editor-process-color: theme("colors.blue.600"); --editor-process-color: #e879f9;
--editor-constant-color: theme("colors.fuchsia.600"); --editor-constant-color: #a78bfa;
--editor-keyword-color: theme("colors.pink.600"); --editor-keyword-color: #f472b6;
} }
@mixin dark-editor-theme { @mixin light-editor-theme {
--editor-type-color: theme("colors.violet.400"); --editor-type-color: #7c3aed;
--editor-name-color: theme("colors.blue.400"); --editor-name-color: #dc2626;
--editor-operator-color: theme("colors.indigo.400"); --editor-operator-color: #4f46e5;
--editor-invalid-color: theme("colors.red.400"); --editor-invalid-color: #dc2626;
--editor-separator-color: theme("colors.gray.400"); --editor-separator-color: #4b5563;
--editor-meta-color: theme("colors.gray.400"); --editor-meta-color: #4b5563;
--editor-variable-color: theme("colors.emerald.400"); --editor-variable-color: #059669;
--editor-link-color: theme("colors.cyan.400"); --editor-link-color: #0891b2;
--editor-process-color: theme("colors.fuchsia.400"); --editor-process-color: #2563eb;
--editor-constant-color: theme("colors.violet.400"); --editor-constant-color: #c026d3;
--editor-keyword-color: theme("colors.pink.400"); --editor-keyword-color: #db2777;
} }
@mixin black-editor-theme { @mixin black-editor-theme {
--editor-type-color: theme("colors.violet.400"); --editor-type-color: #a78bfa;
--editor-name-color: theme("colors.fuchsia.400"); --editor-name-color: #e879f9;
--editor-operator-color: theme("colors.indigo.400"); --editor-operator-color: #818cf8;
--editor-invalid-color: theme("colors.red.400"); --editor-invalid-color: #f87171;
--editor-separator-color: theme("colors.gray.400"); --editor-separator-color: #9ca3af;
--editor-meta-color: theme("colors.gray.400"); --editor-meta-color: #9ca3af;
--editor-variable-color: theme("colors.emerald.400"); --editor-variable-color: #34d399;
--editor-link-color: theme("colors.cyan.400"); --editor-link-color: #22d3ee;
--editor-process-color: theme("colors.violet.400"); --editor-process-color: #a78bfa;
--editor-constant-color: theme("colors.blue.400"); --editor-constant-color: #60a5fa;
--editor-keyword-color: theme("colors.pink.400"); --editor-keyword-color: #f472b6;
} }

View File

@@ -11,7 +11,6 @@
"connect": "Connect", "connect": "Connect",
"connecting": "Connecting", "connecting": "Connecting",
"copy": "Copy", "copy": "Copy",
"create": "Create",
"delete": "Delete", "delete": "Delete",
"disconnect": "Disconnect", "disconnect": "Disconnect",
"dismiss": "Dismiss", "dismiss": "Dismiss",
@@ -41,7 +40,6 @@
"scroll_to_top": "Scroll to top", "scroll_to_top": "Scroll to top",
"search": "Search", "search": "Search",
"send": "Send", "send": "Send",
"share": "Share",
"start": "Start", "start": "Start",
"starting": "Starting", "starting": "Starting",
"stop": "Stop", "stop": "Stop",
@@ -80,7 +78,6 @@
"contact_us": "Contact us", "contact_us": "Contact us",
"cookies": "Cookies", "cookies": "Cookies",
"copy": "Copy", "copy": "Copy",
"copy_interface_type": "Copy interface type",
"copy_user_id": "Copy User Auth Token", "copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options", "developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.", "developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
@@ -96,7 +93,6 @@
"keyboard_shortcuts": "Keyboard shortcuts", "keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch", "name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.", "new_version_found": "New version found. Refresh to update.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options", "options": "Options",
"proxy_privacy_policy": "Proxy privacy policy", "proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload", "reload": "Reload",
@@ -191,7 +187,6 @@
"remove_folder": "Are you sure you want to permanently delete this folder?", "remove_folder": "Are you sure you want to permanently delete this folder?",
"remove_history": "Are you sure you want to permanently delete all history?", "remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?", "remove_request": "Are you sure you want to permanently delete this request?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Are you sure you want to delete this team?", "remove_team": "Are you sure you want to delete this team?",
"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.",
@@ -233,8 +228,7 @@
"profile": "Login to view your profile", "profile": "Login to view your profile",
"protocols": "Protocols are empty", "protocols": "Protocols are empty",
"schema": "Connect to a GraphQL endpoint to view schema", "schema": "Connect to a GraphQL endpoint to view schema",
"shared_requests_logout": "Login to view your shared requests or create a new one", "shortcodes": "Shortcodes are empty",
"shared_requests": "Shared requests are empty",
"subscription": "Subscriptions are empty", "subscription": "Subscriptions are empty",
"team_name": "Team name empty", "team_name": "Team name empty",
"teams": "You don't belong to any teams", "teams": "You don't belong to any teams",
@@ -274,9 +268,6 @@
"variable": "Variable", "variable": "Variable",
"variable_list": "Variable List" "variable_list": "Variable List"
}, },
"graphql_collections": {
"title": "GraphQL Collections"
},
"error": { "error": {
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.", "browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
"check_console_details": "Check console log for details.", "check_console_details": "Check console log for details.",
@@ -312,8 +303,7 @@
"create_secret_gist": "Create secret Gist", "create_secret_gist": "Create secret Gist",
"gist_created": "Gist created", "gist_created": "Gist created",
"require_github": "Login with GitHub to create secret gist", "require_github": "Login with GitHub to create secret gist",
"title": "Export", "title": "Export"
"failed": "Something went wrong while exporting"
}, },
"filter": { "filter": {
"all": "All", "all": "All",
@@ -342,16 +332,20 @@
"url": "URL" "url": "URL"
}, },
"header": { "header": {
"install_pwa": "Install app", "install_pwa": "Add to Home Screen",
"login": "Login", "login": "Login",
"save_workspace": "Save My Workspace" "save_workspace": "Save My Workspace",
"download_app": "Download app",
"menu": "Menu",
"go_back": "Go back",
"go_forward": "Go forward"
}, },
"helpers": { "helpers": {
"authorization": "The authorization header will be automatically generated when you send the request.", "authorization": "The authorization header will be automatically generated when you send the request.",
"generate_documentation_first": "Generate documentation first", "generate_documentation_first": "Generate documentation first",
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.", "network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.", "offline": "You seem to be offline. Data in this workspace might not be up to date.",
"offline_short": "You're using Hoppscotch offline.", "offline_short": "You seem to be offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.", "post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.", "pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.", "script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
@@ -380,7 +374,6 @@
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)", "from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman", "from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection", "from_postman_description": "Import from Postman collection",
"from_file": "Import from File",
"from_url": "Import from URL", "from_url": "Import from URL",
"gist_url": "Enter Gist URL", "gist_url": "Enter Gist URL",
"import_from_url_invalid_fetch": "Couldn't get data from the url", "import_from_url_invalid_fetch": "Couldn't get data from the url",
@@ -388,14 +381,7 @@
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'", "import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported", "import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file", "json_description": "Import collections from a Hoppscotch Collections JSON file",
"title": "Import", "title": "Import"
"hoppscotch_environment": "Hoppscotch Environment",
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment JSON file",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
}, },
"inspections": { "inspections": {
"description": "Inspect possible errors", "description": "Inspect possible errors",
@@ -432,9 +418,7 @@
"close_unsaved_tab": "You have unsaved changes", "close_unsaved_tab": "You have unsaved changes",
"collections": "Collections", "collections": "Collections",
"confirm": "Confirm", "confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request", "edit_request": "Edit Request",
"share_request": "Share Request",
"import_export": "Import / Export" "import_export": "Import / Export"
}, },
"mqtt": { "mqtt": {
@@ -510,6 +494,7 @@
"structured": "Structured", "structured": "Structured",
"text": "Text" "text": "Text"
}, },
"copy_link": "Copy link",
"different_collection": "Cannot reorder requests from different collections", "different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated", "duplicated": "Request duplicated",
"duration": "Duration", "duration": "Duration",
@@ -542,7 +527,6 @@
"saved": "Request saved", "saved": "Request saved",
"share": "Share", "share": "Share",
"share_description": "Share Hoppscotch with your friends", "share_description": "Share Hoppscotch with your friends",
"share_request": "Share Request",
"stop": "Stop", "stop": "Stop",
"title": "Request", "title": "Request",
"type": "Request type", "type": "Request type",
@@ -623,31 +607,14 @@
"additional": "Additional Settings", "additional": "Additional Settings",
"verify_email": "Verify email" "verify_email": "Verify email"
}, },
"shared_requests": { "shortcodes": {
"button": "Button", "actions": "Actions",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.", "created_on": "Created on",
"customize": "Customize", "deleted": "Shortcode deleted",
"creating_widget": "Creating widget", "method": "Method",
"copy_html": "Copy HTML", "not_found": "Shortcode not found",
"copy_link": "Copy Link", "short_code": "Short code",
"copy_markdown": "Copy Markdown", "url": "URL"
"deleted": "Shared request deleted",
"description": "Select a widget, you can change and customize this later",
"embed": "Embed",
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link": "Link",
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
"modified": "Shared request modified",
"not_found": "Shared request not found",
"open_new_tab": "Open in new tab",
"preview": "Preview",
"run_in_hoppscotch": "Run in Hoppscotch",
"theme": {
"dark": "Dark",
"light": "Light",
"system": "System",
"title": "Theme"
}
}, },
"shortcut": { "shortcut": {
"general": { "general": {
@@ -677,6 +644,7 @@
"title": "Others" "title": "Others"
}, },
"request": { "request": {
"copy_request_link": "Copy Request Link",
"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",
@@ -692,7 +660,6 @@
"save_to_collections": "Save to Collections", "save_to_collections": "Save to Collections",
"send_request": "Send Request", "send_request": "Send Request",
"show_code": "Generate code snippet", "show_code": "Generate code snippet",
"share_request": "Share Request",
"title": "Request" "title": "Request"
}, },
"response": { "response": {
@@ -817,7 +784,6 @@
"connection_failed": "Connection failed", "connection_failed": "Connection failed",
"connection_lost": "Connection lost", "connection_lost": "Connection lost",
"copied_to_clipboard": "Copied to clipboard", "copied_to_clipboard": "Copied to clipboard",
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
"deleted": "Deleted", "deleted": "Deleted",
"deprecated": "DEPRECATED", "deprecated": "DEPRECATED",
"disabled": "Disabled", "disabled": "Disabled",
@@ -876,7 +842,6 @@
"queries": "Queries", "queries": "Queries",
"query": "Query", "query": "Query",
"schema": "Schema", "schema": "Schema",
"shared_requests": "Shared Requests",
"socketio": "Socket.IO", "socketio": "Socket.IO",
"sse": "SSE", "sse": "SSE",
"tests": "Tests", "tests": "Tests",

View File

@@ -22,41 +22,45 @@
}, },
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.11.0", "@codemirror/autocomplete": "^6.10.2",
"@codemirror/commands": "^6.3.0", "@codemirror/commands": "^6.3.0",
"@codemirror/lang-javascript": "^6.2.1", "@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2", "@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "6.9.2", "@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "^6.3.3", "@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2", "@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4", "@codemirror/search": "^6.5.4",
"@codemirror/state": "^6.3.1", "@codemirror/state": "^6.3.1",
"@codemirror/view": "^6.22.0", "@codemirror/view": "^6.22.0",
"@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.2.0", "@lezer/highlight": "1.1.4",
"@unhead/vue": "^1.8.8", "@urql/core": "^4.1.1",
"@urql/core": "^4.2.0",
"@urql/devtools": "^2.0.3", "@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6", "@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^6.3.3", "@urql/exchange-graphcache": "^6.3.2",
"@vitejs/plugin-legacy": "^4.1.1", "@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^10.6.1", "@vueuse/core": "^10.3.0",
"acorn-walk": "^8.3.0", "@vueuse/head": "^1.3.1",
"axios": "^1.6.2", "acorn-walk": "^8.2.0",
"axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cookie-es": "^1.0.0", "cookie-es": "^1.0.0",
"dioc": "^1.0.1", "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.16.1",
"fuse.js": "^6.6.2",
"globalthis": "^1.0.3", "globalthis": "^1.0.3",
"graphql": "^16.8.1", "graphql": "^16.8.0",
"graphql-language-service-interface": "^2.10.2", "graphql-language-service-interface": "^2.9.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1", "httpsnippet": "^3.0.1",
"insomnia-importers": "^3.6.0", "insomnia-importers": "^3.6.0",
@@ -64,15 +68,14 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0", "jsonpath-plus": "^7.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lossless-json": "^3.0.2", "lossless-json": "^2.0.11",
"minisearch": "^6.3.0", "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.3.0", "postman-collection": "^4.2.0",
"process": "^0.11.10", "process": "^0.11.10",
"qs": "^6.11.2", "qs": "^6.11.2",
"quicktype-core": "^23.0.79",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5", "set-cookie-parser-es": "^1.0.5",
@@ -86,19 +89,19 @@
"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.3", "url": "^0.11.1",
"util": "^0.12.5", "util": "^0.12.5",
"uuid": "^9.0.0",
"verzod": "^0.2.0", "verzod": "^0.2.0",
"uuid": "^9.0.1", "vue": "^3.3.4",
"vue": "^3.3.8", "vue-i18n": "^9.2.2",
"vue-i18n": "^9.7.1", "vue-pdf-embed": "^1.1.6",
"vue-pdf-embed": "^1.2.1", "vue-router": "^4.2.4",
"vue-router": "^4.2.5",
"vue-tippy": "6.3.1", "vue-tippy": "6.3.1",
"vuedraggable-es": "^4.1.1", "vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4", "wonka": "^6.3.4",
"workbox-window": "^7.0.0", "workbox-window": "^7.0.0",
"xml-formatter": "^3.6.0", "xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1", "yargs-parser": "^21.1.1",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
@@ -110,58 +113,57 @@
"@graphql-codegen/typed-document-node": "^5.0.1", "@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1", "@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1", "@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0", "@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
"@graphql-codegen/urql-introspection": "^3.0.0", "@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-typed-document-node/core": "^3.2.0", "@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.141", "@iconify-json/lucide": "^1.1.119",
"@intlify/vite-plugin-vue-i18n": "^7.0.0", "@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1", "@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.6.0", "@rushstack/eslint-patch": "^1.3.3",
"@types/har-format": "^1.2.15", "@types/har-format": "^1.2.12",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.8",
"@types/lossless-json": "^1.0.4", "@types/lossless-json": "^1.0.1",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.0",
"@types/paho-mqtt": "^1.0.10", "@types/paho-mqtt": "^1.0.7",
"@types/postman-collection": "^3.5.10", "@types/postman-collection": "^3.5.7",
"@types/splitpanes": "^2.2.6", "@types/splitpanes": "^2.2.1",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.2",
"@types/yargs-parser": "^21.0.3", "@types/yargs-parser": "^21.0.0",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.12.0", "@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.8", "@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.3.8", "@vue/runtime-core": "^3.3.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.54.0", "eslint": "^8.47.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.18.1", "eslint-plugin-vue": "^9.17.0",
"glob": "^10.3.10", "glob": "^10.3.10",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.6",
"prettier-plugin-tailwindcss": "^0.5.7", "rollup-plugin-polyfill-node": "^0.12.0",
"rollup-plugin-polyfill-node": "^0.13.0", "sass": "^1.66.0",
"sass": "^1.69.5",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.3.2", "typescript": "^5.1.6",
"unplugin-fonts": "^1.1.1", "unplugin-fonts": "^1.0.3",
"unplugin-icons": "^0.17.4", "unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.2", "unplugin-vue-components": "^0.25.1",
"vite": "^4.5.0", "vite": "^4.4.9",
"vite-plugin-checker": "^0.6.2", "vite-plugin-checker": "^0.6.1",
"vite-plugin-fonts": "^0.7.0", "vite-plugin-fonts": "^0.6.0",
"vite-plugin-html-config": "^1.0.11", "vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.42", "vite-plugin-inspect": "^0.7.38",
"vite-plugin-pages": "^0.31.0", "vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1", "vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.17.0", "vite-plugin-pwa": "^0.16.4",
"vite-plugin-vue-layouts": "^0.8.0", "vite-plugin-vue-layouts": "^0.8.0",
"vitest": "^0.34.6", "vitest": "^0.34.2",
"vue-tsc": "^1.8.22" "vue-tsc": "^1.8.8"
} }
} }

View File

@@ -61,7 +61,6 @@ declare module 'vue' {
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default'] CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default'] CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Embeds: typeof import('./components/embeds/index.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'] EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default'] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
@@ -109,7 +108,6 @@ declare module 'vue' {
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing'] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio'] HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup'] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
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']
@@ -162,14 +160,6 @@ declare module 'vue' {
IconLucideRss: typeof import('~icons/lucide/rss')['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']
IconLucideX: typeof import('~icons/lucide/x')['default']
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default'] InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['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']
@@ -193,16 +183,6 @@ declare module 'vue' {
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default'] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default'] SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default'] SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
Share: typeof import('./components/share/index.vue')['default']
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
ShareModal: typeof import('./components/share/Modal.vue')['default']
ShareRequest: typeof import('./components/share/Request.vue')['default']
ShareRequestModal: typeof import('./components/share/RequestModal.vue')['default']
ShareShareRequestModal: typeof import('./components/share/ShareRequestModal.vue')['default']
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
ShareTemplatesLink: typeof import('./components/share/templates/Link.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']
@@ -223,7 +203,6 @@ declare module 'vue' {
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']
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default'] SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default'] SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default'] SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']

View File

@@ -1,23 +1,19 @@
<template> <template>
<div <div
:role="bannerRole" :role="bannerRole"
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark" class="flex items-center px-4 py-2 text-tiny"
:class="bannerColor" :class="bannerColor"
> >
<div class="flex items-center"> <component :is="bannerIcon" class="mr-2 text-white" />
<component :is="bannerIcon" class="mr-2" />
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }"> <span class="text-white">
{{ banner.text(t) }} <span v-if="banner.alternateText" class="md:hidden">
</span>
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
{{ banner.alternateText(t) }} {{ banner.alternateText(t) }}
</span> </span>
</div> <span :class="banner.alternateText ? '<md:hidden' : ''">
<icon-lucide-x {{ banner.text(t) }}
v-if="dismissible" </span>
class="opacity-50 hover:cursor-pointer hover:opacity-100" </span>
@click="emit('dismiss')"
/>
</div> </div>
</template> </template>
@@ -30,32 +26,22 @@ import IconAlertCircle from "~icons/lucide/alert-circle"
import IconAlertTriangle from "~icons/lucide/alert-triangle" import IconAlertTriangle from "~icons/lucide/alert-triangle"
import IconInfo from "~icons/lucide/info" import IconInfo from "~icons/lucide/info"
const props = withDefaults( const props = defineProps<{
defineProps<{ banner: BannerContent
banner: BannerContent }>()
dismissible?: boolean
}>(),
{
dismissible: false,
}
)
const t = useI18n() const t = useI18n()
const emit = defineEmits<{
(e: "dismiss"): void
}>()
const ariaRoles: Record<BannerType, string> = { const ariaRoles: Record<BannerType, string> = {
info: "status",
warning: "status",
error: "alert", error: "alert",
warning: "status",
info: "status",
} }
const bgColors: Record<BannerType, string> = { const bgColors: Record<BannerType, string> = {
info: "bg-bannerInfo", error: "bg-red-700",
warning: "bg-bannerWarning", warning: "bg-yellow-700",
error: "bg-bannerError", info: "bg-stone-800",
} }
const icons = { const icons = {

View File

@@ -13,16 +13,38 @@
}" }"
> >
<div class="flex"> <div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('header.menu')"
:icon="IconMenu"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
<HoppButtonSecondary <HoppButtonSecondary
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark" class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')" :label="t('app.name')"
to="/" to="/"
/> />
</div> </div>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('header.go_back')"
:icon="IconArrowLeft"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="router.back()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('header.go_forward')"
:icon="IconArrowRight"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="router.forward()"
/>
</div>
</div> </div>
<div class="col-span-1 flex items-center justify-between space-x-2"> <div class="col-span-1 flex items-center justify-between space-x-2">
<button <button
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary" class="flex h-full flex-1 cursor-text items-center justify-between rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')" @click="invokeAction('modals.search.toggle')"
> >
<span class="inline-flex flex-1 items-center"> <span class="inline-flex flex-1 items-center">
@@ -37,14 +59,6 @@
</div> </div>
<div class="col-span-2 flex items-center justify-between space-x-2"> <div class="col-span-2 flex items-center justify-between space-x-2">
<div class="flex"> <div class="flex">
<HoppButtonSecondary
v-if="showInstallButton"
v-tippy="{ theme: 'tooltip' }"
:title="t('header.install_pwa')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/>
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }" v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${ :title="`${
@@ -54,6 +68,41 @@
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')"
/> />
<span>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => downloadActions.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('header.download_app')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
<template #content="{ hide }">
<div
ref="downloadActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
@click="hide()"
>
<HoppSmartItem
:label="t('header.download_app')"
:icon="IconDownload"
/>
<HoppSmartItem
v-if="showInstallButton"
:label="t('header.install_pwa')"
:icon="IconPlusSquare"
@click="installPWA()"
/>
</div>
</template>
</tippy>
</span>
</div> </div>
<div class="flex"> <div class="flex">
<div <div
@@ -113,17 +162,13 @@
theme="popover" theme="popover"
:on-shown="() => accountActions.focus()" :on-shown="() => accountActions.focus()"
> >
<HoppSmartSelectWrapper <HoppButtonSecondary
class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600" v-tippy="{ theme: 'tooltip' }"
> :title="t('workspace.change')"
<HoppButtonSecondary :label="mdAndLarger ? workspaceName : ``"
v-tippy="{ theme: 'tooltip' }" :icon="workspace.type === 'personal' ? IconUser : IconUsers"
:title="t('workspace.change')" class="select-wrapper !focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
:label="mdAndLarger ? workspaceName : ``" />
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
class="!focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
/>
</HoppSmartSelectWrapper>
<template #content="{ hide }"> <template #content="{ hide }">
<div <div
ref="accountActions" ref="accountActions"
@@ -144,10 +189,15 @@
:on-shown="() => tippyActions.focus()" :on-shown="() => tippyActions.focus()"
> >
<HoppSmartPicture <HoppSmartPicture
v-if="currentUser.photoURL"
v-tippy="{ v-tippy="{
theme: 'tooltip', theme: 'tooltip',
}" }"
:name="currentUser.uid" :url="currentUser.photoURL"
:alt="
currentUser.displayName ||
t('profile.default_hopp_displayname')
"
:title=" :title="
currentUser.displayName || currentUser.displayName ||
currentUser.email || currentUser.email ||
@@ -155,7 +205,21 @@
" "
indicator indicator
:indicator-styles=" :indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500' network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
"
/>
<HoppSmartPicture
v-else
v-tippy="{ theme: 'tooltip' }"
:title="
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
:initial="currentUser.displayName || currentUser.email"
indicator
:indicator-styles="
network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
" "
/> />
<template #content="{ hide }"> <template #content="{ hide }">
@@ -168,16 +232,14 @@
@keyup.l="logout.$el.click()" @keyup.l="logout.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<div class="flex flex-col px-2"> <div class="flex flex-col px-2 text-tiny">
<span class="inline-flex truncate font-semibold"> <span class="inline-flex truncate font-semibold">
{{ {{
currentUser.displayName || currentUser.displayName ||
t("profile.default_hopp_displayname") t("profile.default_hopp_displayname")
}} }}
</span> </span>
<span <span class="inline-flex truncate text-secondaryLight">
class="inline-flex truncate text-secondaryLight text-tiny"
>
{{ currentUser.email }} {{ currentUser.email }}
</span> </span>
</div> </div>
@@ -211,12 +273,7 @@
</div> </div>
</div> </div>
</header> </header>
<AppBanner <AppBanner v-if="bannerContent" :banner="bannerContent" />
v-if="bannerContent"
:banner="bannerContent"
:dismissible="true"
@dismiss="dismissOfflineBanner"
/>
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" /> <TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
<TeamsInvite <TeamsInvite
v-if="workspace.type === 'team' && workspace.teamID" v-if="workspace.type === 'team' && workspace.teamID"
@@ -261,6 +318,10 @@ import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
import IconUserPlus from "~icons/lucide/user-plus" import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users" import IconUsers from "~icons/lucide/users"
import IconPlusSquare from "~icons/lucide/plus-square"
import IconArrowLeft from "~icons/lucide/arrow-left"
import IconArrowRight from "~icons/lucide/arrow-right"
import IconMenu from "~icons/lucide/align-left"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team" import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
@@ -269,9 +330,11 @@ import {
BannerContent, BannerContent,
BANNER_PRIORITY_HIGH, BANNER_PRIORITY_HIGH,
} from "~/services/banner.service" } from "~/services/banner.service"
import { useRouter } from "vue-router"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const router = useRouter()
/** /**
* Once the PWA code is initialized, this holds a method * Once the PWA code is initialized, this holds a method
@@ -291,7 +354,7 @@ const bannerContent = computed(() => banner.content.value?.content)
let bannerID: number | null = null let bannerID: number | null = null
const offlineBanner: BannerContent = { const offlineBanner: BannerContent = {
type: "warning", type: "info",
text: (t) => t("helpers.offline"), text: (t) => t("helpers.offline"),
alternateText: (t) => t("helpers.offline_short"), alternateText: (t) => t("helpers.offline_short"),
score: BANNER_PRIORITY_HIGH, score: BANNER_PRIORITY_HIGH,
@@ -305,14 +368,13 @@ watch(isOnline, () => {
if (!isOnline.value) { if (!isOnline.value) {
bannerID = banner.showBanner(offlineBanner) bannerID = banner.showBanner(offlineBanner)
return return
} } else {
if (banner.content && bannerID) { if (banner.content && bannerID) {
banner.removeBanner(bannerID) banner.removeBanner(bannerID)
}
} }
}) })
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(), platform.auth.getProbableUserStream(),
platform.auth.getProbableUser() platform.auth.getProbableUser()
@@ -444,6 +506,7 @@ 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)
const downloadActions = ref<any | null>(null)
defineActionHandler("modals.team.edit", handleTeamEdit) defineActionHandler("modals.team.edit", handleTeamEdit)

View File

@@ -92,8 +92,9 @@ const getHighestSeverity = computed(() => {
}, },
{ severity: 0 } { severity: 0 }
) )
} else {
return { severity: 0 }
} }
return { severity: 0 }
}) })
const severityColor = (severity: number) => { const severityColor = (severity: number) => {

View File

@@ -17,10 +17,9 @@
v-if="isEmpty(shortcutsResults)" v-if="isEmpty(shortcutsResults)"
:text="`${t('state.nothing_found')} ‟${filterText}”`" :text="`${t('state.nothing_found')} ‟${filterText}”`"
> >
<template #icon> <icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<details <details
v-for="(sectionResults, sectionTitle) in shortcutsResults" v-for="(sectionResults, sectionTitle) in shortcutsResults"
v-else v-else

View File

@@ -9,8 +9,8 @@
> >
<component <component
:is="entry.icon" :is="entry.icon"
class="svg-icons opacity-80" class="svg-icons opacity-50"
:class="{ 'opacity-25': active }" :class="{ 'opacity-100': active }"
/> />
<template <template
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'" v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
@@ -82,9 +82,9 @@ const props = defineProps<{
const formattedShortcutKeys = computed( const formattedShortcutKeys = computed(
() => () =>
props.entry.meta?.keyboardShortcut?.map( props.entry.meta?.keyboardShortcut?.map((key) => {
(key) => SPECIAL_KEY_CHARS[key] ?? capitalize(key) return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
) })
) )
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -16,7 +16,7 @@
autocomplete="off" autocomplete="off"
name="command" name="command"
:placeholder="`${t('app.type_a_command_search')}`" :placeholder="`${t('app.type_a_command_search')}`"
class="flex flex-1 bg-transparent px-6 pt-5 pb-3 text-base text-secondaryDark" class="flex flex-1 bg-transparent px-6 py-5 text-base text-secondaryDark"
/> />
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" /> <HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
</div> </div>
@@ -49,15 +49,13 @@
:text="`${t('state.nothing_found')} ‟${search}”`" :text="`${t('state.nothing_found')} ‟${search}”`"
> >
<template #icon> <template #icon>
<icon-lucide-search class="svg-icons opacity-75" /> <icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
<template #body>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="search = ''"
/>
</template> </template>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="search = ''"
/>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div> </div>
<div <div
@@ -114,7 +112,6 @@ import {
WorkspaceSpotlightSearcherService, WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher" } from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher" import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
@@ -144,10 +141,6 @@ useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService) useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService) useService(InterceptorSpotlightSearcherService)
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
useService(searcher)
)
const search = ref("") const search = ref("")
const searchSession = ref<SpotlightSearchState>() const searchSession = ref<SpotlightSearchState>()

View File

@@ -14,14 +14,14 @@
></div> ></div>
<div class="relative flex flex-col"> <div class="relative flex flex-col">
<div <div
class="z-[1] pointer-events-none absolute inset-0 bg-accent opacity-0 transition" class="z-1 pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
:class="{ :class="{
'opacity-25': 'opacity-25':
dragging && notSameDestination && notSameParentDestination, dragging && notSameDestination && notSameParentDestination,
}" }"
></div> ></div>
<div <div
class="z-[3] group pointer-events-auto relative flex cursor-pointer items-stretch" class="z-3 group pointer-events-auto relative flex cursor-pointer items-stretch"
:draggable="!hasNoTeamAccess" :draggable="!hasNoTeamAccess"
@dragstart="dragStart" @dragstart="dragStart"
@drop="handelDrop($event)" @drop="handelDrop($event)"
@@ -290,13 +290,13 @@ const collectionIcon = computed(() => {
if (props.isSelected) return IconCheckCircle if (props.isSelected) return IconCheckCircle
else if (!props.isOpen) return IconFolder else if (!props.isOpen) return IconFolder
else if (props.isOpen) return IconFolderOpen else if (props.isOpen) return IconFolderOpen
return IconFolder else return IconFolder
}) })
const collectionName = computed(() => { const collectionName = computed(() => {
if ((props.data as HoppCollection<HoppRESTRequest>).name) if ((props.data as HoppCollection<HoppRESTRequest>).name)
return (props.data as HoppCollection<HoppRESTRequest>).name return (props.data as HoppCollection<HoppRESTRequest>).name
return (props.data as TeamCollection).title else return (props.data as TeamCollection).title
}) })
watch( watch(
@@ -424,8 +424,9 @@ const isCollLoading = computed(() => {
props.data.id props.data.id
) { ) {
return collectionMoveLoading.includes(props.data.id) return collectionMoveLoading.includes(props.data.id)
} else {
return false
} }
return false
}) })
const resetDragState = () => { const resetDragState = () => {

View File

@@ -1,568 +1,361 @@
<template> <template>
<ImportExportBase <HoppSmartModal
ref="collections-import-export" v-if="show"
modal-title="modal.collections" dialog
:importer-modules="importerModules" :title="t('modal.collections')"
:exporter-modules="exporterModules" styles="sm:max-w-md"
@hide-modal="emit('hide-modal')" @close="hideModal"
/> >
<template #actions>
<HoppButtonSecondary
v-if="importerType !== null"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.go_back')"
:icon="IconArrowLeft"
@click="resetImport"
/>
</template>
<template #body>
<div v-if="importerType !== null" class="flex flex-col">
<div class="flex flex-col pb-4">
<div
v-for="(step, index) in importerSteps"
:key="`step-${index}`"
class="flex flex-col space-y-8"
>
<div v-if="step.name === 'FILE_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
:class="{
'!text-green-500': hasFile,
}"
>
<icon-lucide-check-circle class="svg-icons" />
</span>
<span>
{{ t(`${step.metadata.caption}`) }}
</span>
</p>
<p
class="ml-10 flex flex-col rounded border border-dashed border-dividerDark"
>
<input
id="inputChooseFileToImportFrom"
ref="inputChooseFileToImportFrom"
name="inputChooseFileToImportFrom"
type="file"
class="cursor-pointer p-4 text-secondary transition file:mr-2 file:cursor-pointer file:rounded file:border-0 file:bg-primaryLight file:px-4 file:py-2 file:text-secondary file:transition hover:text-secondaryDark hover:file:bg-primaryDark hover:file:text-secondaryDark"
:accept="step.metadata.acceptedFileTypes"
@change="onFileChange"
/>
</p>
</div>
<div v-else-if="step.name === 'URL_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
:class="{
'!text-green-500': hasGist,
}"
>
<icon-lucide-check-circle class="svg-icons" />
</span>
<span>
{{ t(`${step.metadata.caption}`) }}
</span>
</p>
<p class="ml-10 flex flex-col">
<input
v-model="inputChooseGistToImportFrom"
type="url"
class="input"
:placeholder="`${t('import.gist_url')}`"
/>
</p>
</div>
<div
v-else-if="step.name === 'TARGET_MY_COLLECTION'"
class="flex flex-col"
>
<div class="select-wrapper">
<select
v-model="mySelectedCollectionID"
autocomplete="off"
class="select"
autofocus
>
<option :key="undefined" :value="undefined" disabled selected>
{{ t("collection.select") }}
</option>
<option
v-for="(collection, collectionIndex) in myCollections"
:key="`collection-${collectionIndex}`"
:value="collectionIndex"
class="bg-primary"
>
{{ collection.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<HoppButtonPrimary
:label="t('import.title')"
:disabled="enableImportButton"
:loading="importingMyCollections"
@click="finishImport"
/>
</div>
<div v-else class="flex flex-col">
<HoppSmartExpand>
<template #body>
<HoppSmartItem
v-for="(importer, index) in importerModules"
:key="`importer-${index}`"
:icon="importer.icon"
:label="t(`${importer.name}`)"
@click="importerType = index"
/>
</template>
</HoppSmartExpand>
<hr />
<div class="flex flex-col space-y-2">
<HoppSmartItem
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:icon="IconDownload"
:loading="exportingTeamCollections"
:label="t('export.as_json')"
@click="emit('export-json-collection')"
/>
<span
v-if="platform.platformFeatureFlags.exportAsGIST"
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? `${t('export.require_github')}`
: currentUser.provider !== 'github.com'
? `${t('export.require_github')}`
: undefined
"
class="flex"
>
<HoppSmartItem
:disabled="
!currentUser
? true
: currentUser.provider !== 'github.com'
? true
: false
"
:icon="IconGithub"
:loading="creatingGistCollection"
:label="t('export.create_secret_gist')"
@click="emit('create-collection-gist')"
/>
</span>
</div>
</div>
</template>
</HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as E from "fp-ts/Either" import IconArrowLeft from "~icons/lucide/arrow-left"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import IconDownload from "~icons/lucide/download"
import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
import IconFile from "~icons/lucide/file"
import {
hoppRESTImporter,
hoppInsomniaImporter,
hoppPostmanImporter,
toTeamsImporter,
hoppOpenAPIImporter,
} from "~/helpers/import-export/import/importers"
import { defineStep } from "~/composables/step-components"
import { PropType, computed, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { HoppCollection } from "@hoppscotch/data"
import { HoppRESTRequest } from "@hoppscotch/data"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconOpenAPI from "~icons/lucide/file"
import IconPostman from "~icons/hopp/postman"
import IconInsomnia from "~icons/hopp/insomnia"
import IconGithub from "~icons/lucide/github" import IconGithub from "~icons/lucide/github"
import IconLink from "~icons/lucide/link" import { computed, PropType, ref, watch } from "vue"
import { pipe } from "fp-ts/function"
import IconUser from "~icons/lucide/user" import * as E from "fp-ts/Either"
import { useReadonlyStream } from "~/composables/stream" import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
import { getTeamCollectionJSON } from "~/helpers/backend/helpers" import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import { platform } from "~/platform" import { platform } from "~/platform"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
import { StepReturnValue } from "~/helpers/import-export/steps"
import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { collectionsGistExporter } from "~/helpers/import-export/export/gistExport"
import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { ImporterOrExporter } from "~/components/importExport/types"
const t = useI18n()
const toast = useToast() const toast = useToast()
const t = useI18n()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined type CollectionType = "team-collections" | "my-collections"
type CollectionType =
| {
type: "team-collections"
selectedTeam: SelectedTeam
}
| { type: "my-collections" }
const props = defineProps({ const props = defineProps({
collectionsType: { show: {
type: Object as PropType<CollectionType>, type: Boolean,
default: () => ({ default: false,
type: "my-collections",
selectedTeam: undefined,
}),
required: true, required: true,
}, },
collectionsType: {
type: String as PropType<CollectionType>,
default: "my-collections",
required: true,
},
exportingTeamCollections: {
type: Boolean,
default: false,
required: false,
},
creatingGistCollection: {
type: Boolean,
default: false,
required: false,
},
importingMyCollections: {
type: Boolean,
default: false,
required: false,
},
}) })
const emit = defineEmits<{
(e: "hide-modal"): void
(e: "update-team-collections"): void
(e: "export-json-collection"): void
(e: "create-collection-gist"): void
(e: "import-to-teams", payload: HoppCollection<HoppRESTRequest>[]): void
}>()
const hasFile = ref(false)
const hasGist = ref(false)
const importerType = ref<number | null>(null)
const stepResults = ref<StepReturnValue[]>([])
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
const mySelectedCollectionID = ref<number | undefined>(undefined)
const inputChooseGistToImportFrom = ref<string>("")
const importerModules = computed(() =>
RESTCollectionImporters.filter(
(i) => i.applicableTo?.includes(props.collectionsType) ?? true
)
)
const importerModule = computed(() => {
if (importerType.value === null) return null
return importerModules.value[importerType.value]
})
const importerSteps = computed(() => importerModule.value?.steps ?? null)
const enableImportButton = computed(
() => !(stepResults.value.length === importerSteps.value?.length)
)
watch(mySelectedCollectionID, (newValue) => {
if (newValue === undefined) return
stepResults.value = []
stepResults.value.push(newValue)
})
watch(inputChooseGistToImportFrom, (url) => {
stepResults.value = []
if (url === "") {
hasGist.value = false
} else {
hasGist.value = true
stepResults.value.push(inputChooseGistToImportFrom.value)
}
})
const myCollections = useReadonlyStream(restCollections$, [])
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(), platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser() platform.auth.getCurrentUser()
) )
const showImportFailedError = () => { const importerAction = async (stepResults: StepReturnValue[]) => {
toast.error(t("import.failed")) if (!importerModule.value) return
}
const handleImportToStore = async ( pipe(
collections: HoppCollection<HoppRESTRequest>[] await importerModule.value.importer(stepResults as any)(),
) => { E.match(
const importResult = (err) => {
props.collectionsType.type === "my-collections" failedImport()
? await importToPersonalWorkspace(collections) console.error("error", err)
: await importToTeamsWorkspace(collections) },
(result) => {
if (props.collectionsType === "team-collections") {
emit("import-to-teams", result)
} else {
appendRESTCollections(result)
if (E.isRight(importResult)) { platform.analytics?.logEvent({
toast.success(t("state.file_imported")) type: "HOPP_IMPORT_COLLECTION",
emit("hide-modal") importer: importerModule.value!.name,
} else { platform: "rest",
toast.error(t("import.failed")) workspaceType: "personal",
} })
}
const importToPersonalWorkspace = ( fileImported()
collections: HoppCollection<HoppRESTRequest>[]
) => {
appendRESTCollections(collections)
return E.right({
success: true,
})
}
const importToTeamsWorkspace = async (
collections: HoppCollection<HoppRESTRequest>[]
) => {
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
return E.left({
success: false,
})
}
const res = await toTeamsImporter(
JSON.stringify(collections),
selectedTeamID.value
)()
return E.isRight(res)
? E.right({ success: true })
: E.left({
success: false,
})
}
const emit = defineEmits<{
(e: "hide-modal"): () => void
}>()
const isHoppMyCollectionExporterInProgress = ref(false)
const isHoppTeamCollectionExporterInProgress = ref(false)
const isHoppGistCollectionExporterInProgress = ref(false)
const isTeamWorkspace = computed(() => {
return props.collectionsType.type === "team-collections"
})
const HoppRESTImporter: ImporterOrExporter = {
metadata: {
id: "hopp_rest",
name: "import.from_json",
title: "import.from_json_description",
icon: IconFolderPlus,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json",
onImportFromFile: async (content) => {
const res = await hoppRESTImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_json",
platform: "rest",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppMyCollectionImporter: ImporterOrExporter = {
metadata: {
id: "hopp_my_collection",
name: "import.from_my_collections",
title: "import.from_my_collections_description",
icon: IconUser,
disabled: false,
applicableTo: ["team-workspace"],
},
component: defineStep("my_collection_import", MyCollectionImport, () => ({
async onImportFromMyCollection(content) {
handleImportToStore([content])
// our analytics consider this as an export event, so keeping compatibility with that
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "import_to_teams",
platform: "rest",
})
},
})),
}
const HoppOpenAPIImporter: ImporterOrExporter = {
metadata: {
id: "hopp_openapi",
name: "import.from_openapi",
title: "import.from_openapi_description",
icon: IconOpenAPI,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
supported_sources: [
{
id: "file_import",
name: "import.from_file",
icon: IconFile,
step: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json, .yaml, .yml",
onImportFromFile: async (content) => {
const res = await hoppOpenAPIImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_openapi",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
},
{
id: "url_import",
name: "import.from_url",
icon: IconLink,
step: UrlSource({
caption: "import.from_url",
onImportFromURL: async (content) => {
const res = await hoppOpenAPIImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_openapi",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
},
],
}
const HoppPostmanImporter: ImporterOrExporter = {
metadata: {
id: "hopp_postman",
name: "import.from_postman",
title: "import.from_postman_description",
icon: IconPostman,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json",
onImportFromFile: async (content) => {
const res = await hoppPostmanImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_postman",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppInsomniaImporter: ImporterOrExporter = {
metadata: {
id: "hopp_insomnia",
name: "import.from_insomnia",
title: "import.from_insomnia_description",
icon: IconInsomnia,
disabled: true,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json",
onImportFromFile: async (content) => {
const res = await hoppInsomniaImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_insomnia",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppGistImporter: ImporterOrExporter = {
metadata: {
id: "hopp_gist",
name: "import.from_gist",
title: "import.from_gist_description",
icon: IconGithub,
disabled: true,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: GistSource({
caption: "import.from_url",
onImportFromGist: async (content) => {
if (E.isLeft(content)) {
showImportFailedError()
return
}
const res = await hoppRESTImporter(content.right)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_gist",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppMyCollectionsExporter: ImporterOrExporter = {
metadata: {
id: "hopp_my_collections",
name: "export.as_json",
title: "action.download_file",
icon: IconUser,
disabled: false,
applicableTo: ["personal-workspace"],
isLoading: isHoppMyCollectionExporterInProgress,
},
action: () => {
if (!myCollections.value.length) {
return toast.error(t("error.no_collections_to_export"))
}
isHoppMyCollectionExporterInProgress.value = true
const message = initializeDownloadCollection(
myCollectionsExporter(myCollections.value),
"Collections"
)
if (E.isRight(message)) {
toast.success(t(message.right))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
}
isHoppMyCollectionExporterInProgress.value = false
},
}
const HoppTeamCollectionsExporter: ImporterOrExporter = {
metadata: {
id: "hopp_team_collections",
name: "export.as_json",
title: "export.as_json_description",
icon: IconUser,
disabled: false,
applicableTo: ["team-workspace"],
isLoading: isHoppTeamCollectionExporterInProgress,
},
action: async () => {
isHoppTeamCollectionExporterInProgress.value = true
if (
props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam
) {
const res = await teamCollectionsExporter(
props.collectionsType.selectedTeam.id
)
if (E.isRight(res)) {
const { exportCollectionsToJSON } = res.right
if (!JSON.parse(exportCollectionsToJSON).length) {
isHoppTeamCollectionExporterInProgress.value = false
return toast.error(t("error.no_collections_to_export"))
} }
initializeDownloadCollection(
exportCollectionsToJSON,
"team-collections"
)
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
} else {
toast.error(res.left.error.toString())
} }
} )
)
isHoppTeamCollectionExporterInProgress.value = false
},
} }
const HoppGistCollectionsExporter: ImporterOrExporter = { const finishImport = async () => {
metadata: { await importerAction(stepResults.value)
id: "create_secret_gist", }
name: "export.create_secret_gist",
icon: IconGithub,
disabled: !currentUser.value
? true
: currentUser.value.provider !== "github.com",
title: t("export.create_secret_gist"),
applicableTo: ["personal-workspace", "team-workspace"],
isLoading: isHoppGistCollectionExporterInProgress,
},
action: async () => {
isHoppGistCollectionExporterInProgress.value = true
const collectionJSON = await getCollectionJSON() const onFileChange = () => {
const accessToken = currentUser.value?.accessToken stepResults.value = []
if (!accessToken) { const inputFileToImport = inputChooseFileToImportFrom.value[0]
toast.error(t("error.something_went_wrong"))
isHoppGistCollectionExporterInProgress.value = false if (!inputFileToImport) {
hasFile.value = false
return
}
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
inputChooseFileToImportFrom.value[0].value = ""
hasFile.value = false
toast.show(t("action.choose_file").toString())
return
}
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target!.result as string | null
if (!content) {
hasFile.value = false
toast.show(t("action.choose_file").toString())
return return
} }
if (E.isRight(collectionJSON)) { stepResults.value.push(content)
collectionsGistExporter(collectionJSON.right, accessToken) hasFile.value = !!content?.length
}
platform.analytics?.logEvent({ reader.readAsText(inputFileToImport.files[0])
type: "HOPP_EXPORT_COLLECTION",
exporter: "gist",
platform: "rest",
})
}
isHoppGistCollectionExporterInProgress.value = false
},
} }
const importerModules = computed(() => { const fileImported = () => {
const enabledImporters = [ toast.success(t("state.file_imported").toString())
HoppRESTImporter, hideModal()
HoppMyCollectionImporter, }
HoppOpenAPIImporter, const failedImport = () => {
HoppPostmanImporter, toast.error(t("import.failed").toString())
HoppInsomniaImporter, }
HoppGistImporter, const hideModal = () => {
] resetImport()
emit("hide-modal")
}
const isTeams = props.collectionsType.type === "team-collections" const resetImport = () => {
importerType.value = null
return enabledImporters.filter((importer) => { hasFile.value = false
return isTeams hasGist.value = false
? importer.metadata.applicableTo.includes("team-workspace") stepResults.value = []
: importer.metadata.applicableTo.includes("personal-workspace") inputChooseFileToImportFrom.value = ""
}) inputChooseGistToImportFrom.value = ""
}) mySelectedCollectionID.value = undefined
const exporterModules = computed(() => {
const enabledExporters = [
HoppMyCollectionsExporter,
HoppTeamCollectionsExporter,
]
if (platform.platformFeatureFlags.exportAsGIST) {
enabledExporters.push(HoppGistCollectionsExporter)
}
return enabledExporters.filter((exporter) => {
return exporter.metadata.applicableTo.includes(
props.collectionsType.type === "my-collections"
? "personal-workspace"
: "team-workspace"
)
})
})
const hasTeamWriteAccess = computed(() => {
const { collectionsType } = props
const isTeamCollection = collectionsType.type === "team-collections"
if (!isTeamCollection || !collectionsType.selectedTeam) {
return false
}
return (
collectionsType.selectedTeam.myRole === "EDITOR" ||
collectionsType.selectedTeam.myRole === "OWNER"
)
})
const selectedTeamID = computed(() => {
const { collectionsType } = props
return collectionsType.type === "team-collections"
? collectionsType.selectedTeam?.id
: undefined
})
const myCollections = useReadonlyStream(restCollections$, [])
const getCollectionJSON = async () => {
if (
props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam?.id
) {
const res = await getTeamCollectionJSON(
props.collectionsType.selectedTeam?.id
)
return E.isRight(res)
? E.right(res.right.exportCollectionsToJSON)
: E.left(res.left)
}
if (props.collectionsType.type === "my-collections") {
return E.right(JSON.stringify(myCollections.value, null, 2))
}
return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE")
} }
</script> </script>

View File

@@ -222,12 +222,6 @@
requestIndex: pathToIndex(node.id), requestIndex: pathToIndex(node.id),
}) })
" "
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data,
})
"
@drag-request=" @drag-request="
dragRequest($event, { dragRequest($event, {
folderPath: node.data.data.parentIndex, folderPath: node.data.data.parentIndex,
@@ -254,7 +248,7 @@
:text="`${t('state.nothing_found')}${filterText}`" :text="`${t('state.nothing_found')}${filterText}`"
> >
<template #icon> <template #icon>
<icon-lucide-search class="svg-icons opacity-75" /> <icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template> </template>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
@@ -263,29 +257,27 @@
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<template #body> <div class="flex flex-col items-center space-y-4">
<div class="flex flex-col items-center space-y-4"> <span class="text-center text-secondaryLight">
<span class="text-center text-secondaryLight"> {{ t("collection.import_or_create") }}
{{ t("collection.import_or_create") }} </span>
</span> <div class="flex flex-col items-stretch gap-4">
<div class="flex flex-col items-stretch gap-4"> <HoppButtonPrimary
<HoppButtonPrimary :icon="IconImport"
:icon="IconImport" :label="t('import.title')"
:label="t('import.title')" filled
filled outline
outline @click="emit('display-modal-import-export')"
@click="emit('display-modal-import-export')" />
/> <HoppButtonSecondary
<HoppButtonSecondary :icon="IconPlus"
:icon="IconPlus" :label="t('add.new')"
:label="t('add.new')" filled
filled outline
outline @click="emit('display-modal-add')"
@click="emit('display-modal-add')" />
/>
</div>
</div> </div>
</template> </div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'" v-else-if="node.data.type === 'collections'"
@@ -293,20 +285,18 @@
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<template #body> <HoppButtonSecondary
<HoppButtonSecondary :label="t('add.new')"
:label="t('add.new')" filled
filled outline
outline @click="
@click=" node.data.type === 'collections' &&
node.data.type === 'collections' && emit('add-folder', {
emit('add-folder', { path: node.id,
path: node.id, folder: node.data.data.data,
folder: node.data.data.data, })
}) "
" />
/>
</template>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'" v-else-if="node.data.type === 'folders'"
@@ -470,12 +460,6 @@ const emit = defineEmits<{
isActive: boolean isActive: boolean
} }
): void ): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
( (
event: "drop-request", event: "drop-request",
payload: { payload: {
@@ -542,12 +526,13 @@ const isSelected = ({
props.picked.folderPath === folderPath && props.picked.folderPath === folderPath &&
props.picked.requestIndex === requestIndex props.picked.requestIndex === requestIndex
) )
} else {
return (
props.picked &&
props.picked.pickedType === "my-folder" &&
props.picked.folderPath === folderPath
)
} }
return (
props.picked &&
props.picked.pickedType === "my-folder" &&
props.picked.folderPath === folderPath
)
} }
const tabs = useService(RESTTabService) const tabs = useService(RESTTabService)
@@ -744,10 +729,11 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
status: "loaded", status: "loaded",
data: data, data: data,
} as ChildrenResult<Folder | Requests> } as ChildrenResult<Folder | Requests>
} } else {
return { return {
status: "loaded", status: "loaded",
data: [], data: [],
}
} }
}) })
} }

View File

@@ -28,7 +28,8 @@
> >
<span <span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2" class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
:style="{ color: getMethodLabelColorClassOf(request) }" :class="requestLabelColor"
:style="{ color: requestLabelColor }"
> >
<component <component
:is="IconCheckCircle" :is="IconCheckCircle"
@@ -93,7 +94,6 @@
@keyup.e="edit?.$el.click()" @keyup.e="edit?.$el.click()"
@keyup.d="duplicate?.$el.click()" @keyup.d="duplicate?.$el.click()"
@keyup.delete="deleteAction?.$el.click()" @keyup.delete="deleteAction?.$el.click()"
@keyup.s="shareAction?.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<HoppSmartItem <HoppSmartItem
@@ -133,18 +133,6 @@
} }
" "
/> />
<HoppSmartItem
ref="shareAction"
:icon="IconShare2"
:label="t('action.share')"
:shortcut="['S']"
@click="
() => {
emit('share-request')
hide()
}
"
/>
</div> </div>
</template> </template>
</tippy> </tippy>
@@ -174,7 +162,6 @@ import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw" import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconShare2 from "~icons/lucide/share-2"
import { ref, PropType, watch, computed } from "vue" import { ref, PropType, watch, computed } from "vue"
import { HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
@@ -253,7 +240,6 @@ const emit = defineEmits<{
(event: "duplicate-request"): void (event: "duplicate-request"): void
(event: "remove-request"): void (event: "remove-request"): void
(event: "select-request"): void (event: "select-request"): void
(event: "share-request"): void
(event: "drag-request", payload: DataTransfer): void (event: "drag-request", payload: DataTransfer): void
(event: "update-request-order", payload: DataTransfer): void (event: "update-request-order", payload: DataTransfer): void
(event: "update-last-request-order", payload: DataTransfer): void (event: "update-last-request-order", payload: DataTransfer): void
@@ -264,7 +250,6 @@ const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null) const deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const duplicate = ref<HTMLButtonElement | null>(null) const duplicate = ref<HTMLButtonElement | null>(null)
const shareAction = ref<HTMLButtonElement | null>(null)
const dragging = ref(false) const dragging = ref(false)
const ordering = ref(false) const ordering = ref(false)
@@ -276,6 +261,10 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
parentID: "", parentID: "",
}) })
const requestLabelColor = computed(() =>
getMethodLabelColorClassOf(props.request)
)
watch( watch(
() => props.duplicateLoading, () => props.duplicateLoading,
(val) => { (val) => {
@@ -374,8 +363,9 @@ const updateLastItemOrder = (e: DragEvent) => {
const isRequestLoading = computed(() => { const isRequestLoading = computed(() => {
if (props.requestMoveLoading.length > 0 && props.requestID) { if (props.requestMoveLoading.length > 0 && props.requestID) {
return props.requestMoveLoading.includes(props.requestID) return props.requestMoveLoading.includes(props.requestID)
} else {
return false
} }
return false
}) })
const resetDragState = () => { const resetDragState = () => {

View File

@@ -141,8 +141,9 @@ const reqName = computed(() => {
return props.request.name return props.request.name
} else if (props.mode === "rest") { } else if (props.mode === "rest") {
return restRequestName.value return restRequestName.value
} else {
return gqlRequestName.value
} }
return gqlRequestName.value
}) })
const requestName = ref(reqName.value) const requestName = ref(reqName.value)
@@ -479,20 +480,21 @@ const getErrorMessage = (err: GQLError<string>) => {
console.error(err) console.error(err)
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_coll/short_title": case "team_coll/short_title":
return t("collection.name_length_insufficient") return t("collection.name_length_insufficient")
case "team/invalid_coll_id": case "team/invalid_coll_id":
return t("team.invalid_id") return t("team.invalid_id")
case "team/not_required_role": case "team/not_required_role":
return t("profile.no_permission") return t("profile.no_permission")
case "team_req/not_required_role": case "team_req/not_required_role":
return t("profile.no_permission") return t("profile.no_permission")
case "Forbidden resource": case "Forbidden resource":
return t("profile.no_permission") return t("profile.no_permission")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }
</script> </script>

View File

@@ -15,12 +15,12 @@
class="!rounded-none" class="!rounded-none"
:icon="IconPlus" :icon="IconPlus"
:title="t('team.no_access')" :title="t('team.no_access')"
:label="t('action.new')" :label="t('add.new')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
:icon="IconPlus" :icon="IconPlus"
:label="t('action.new')" :label="t('add.new')"
class="!rounded-none" class="!rounded-none"
@click="emit('display-modal-add')" @click="emit('display-modal-add')"
/> />
@@ -240,12 +240,6 @@
requestIndex: node.data.data.data.id, requestIndex: node.data.data.data.id,
}) })
" "
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data.request,
})
"
@drag-request=" @drag-request="
dragRequest($event, { dragRequest($event, {
folderPath: node.data.data.parentIndex, folderPath: node.data.data.parentIndex,
@@ -274,37 +268,33 @@
:text="t('empty.collections')" :text="t('empty.collections')"
@drop.stop @drop.stop
> >
<template #body> <div class="flex flex-col items-center space-y-4">
<div class="flex flex-col items-center space-y-4"> <span class="text-center text-secondaryLight">
<span class="text-center text-secondaryLight"> {{ t("collection.import_or_create") }}
{{ t("collection.import_or_create") }} </span>
</span> <div class="flex flex-col items-stretch gap-4">
<div class="flex flex-col items-stretch gap-4"> <HoppButtonPrimary
<HoppButtonPrimary :icon="IconImport"
:icon="IconImport" :label="t('import.title')"
:label="t('import.title')" filled
filled outline
outline :disabled="hasNoTeamAccess"
:disabled="hasNoTeamAccess" :title="hasNoTeamAccess ? t('team.no_access') : ''"
:title="hasNoTeamAccess ? t('team.no_access') : ''" @click="
@click=" hasNoTeamAccess ? null : emit('display-modal-import-export')
hasNoTeamAccess "
? null />
: emit('display-modal-import-export') <HoppButtonSecondary
" :icon="IconPlus"
/> :label="t('add.new')"
<HoppButtonSecondary filled
:icon="IconPlus" outline
:label="t('add.new')" :disabled="hasNoTeamAccess"
filled :title="hasNoTeamAccess ? t('team.no_access') : ''"
outline @click="hasNoTeamAccess ? null : emit('display-modal-add')"
:disabled="hasNoTeamAccess" />
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/>
</div>
</div> </div>
</template> </div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'" v-else-if="node.data.type === 'collections'"
@@ -313,20 +303,18 @@
:text="t('empty.collections')" :text="t('empty.collections')"
@drop.stop @drop.stop
> >
<template #body> <HoppButtonSecondary
<HoppButtonSecondary :label="t('add.new')"
:label="t('add.new')" filled
filled outline
outline @click="
@click=" node.data.type === 'collections' &&
node.data.type === 'collections' && emit('add-folder', {
emit('add-folder', { path: node.id,
path: node.id, folder: node.data.data.data,
folder: node.data.data.data, })
}) "
" />
/>
</template>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'" v-else-if="node.data.type === 'folders'"
@@ -485,12 +473,6 @@ const emit = defineEmits<{
folderPath?: string | undefined folderPath?: string | undefined
} }
): void ): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
( (
event: "drop-request", event: "drop-request",
payload: { payload: {
@@ -560,12 +542,13 @@ const isSelected = ({
props.picked.pickedType === "teams-request" && props.picked.pickedType === "teams-request" &&
props.picked.requestID === requestID props.picked.requestID === requestID
) )
} else {
return (
props.picked &&
props.picked.pickedType === "teams-folder" &&
props.picked.folderID === folderID
)
} }
return (
props.picked &&
props.picked.pickedType === "teams-folder" &&
props.picked.folderID === folderID
)
} }
const active = computed(() => tabs.currentActiveTab.value.document.saveContext) const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
@@ -731,77 +714,81 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
return { return {
status: "loading", status: "loading",
} }
} } else {
const data = this.data.value.map((item, index) => ({ const data = this.data.value.map((item, index) => ({
id: item.id, id: item.id,
data: {
isLastItem: index === this.data.value.length - 1,
type: "collections",
data: { data: {
parentIndex: null, isLastItem: index === this.data.value.length - 1,
data: item, type: "collections",
data: {
parentIndex: null,
data: item,
},
}, },
}, }))
})) return {
return { status: "loaded",
status: "loaded", data: cloneDeep(data),
data: cloneDeep(data), } as ChildrenResult<TeamCollections>
} as ChildrenResult<TeamCollections> }
} } else {
const parsedID = id.split("/")[id.split("/").length - 1] const parsedID = id.split("/")[id.split("/").length - 1]
!props.teamLoadingCollections.includes(parsedID) && !props.teamLoadingCollections.includes(parsedID) &&
emit("expand-team-collection", parsedID) emit("expand-team-collection", parsedID)
if (props.teamLoadingCollections.includes(parsedID)) { if (props.teamLoadingCollections.includes(parsedID)) {
return { return {
status: "loading", status: "loading",
}
} else {
const items = this.findCollInTree(this.data.value, parsedID)
if (items) {
const data = [
...(items.children
? items.children.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.children && items.children.length > 1
? index === items.children.length - 1
: false,
type: "folders",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
...(items.requests
? items.requests.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.requests && items.requests.length > 1
? index === items.requests.length - 1
: false,
type: "requests",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
]
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamFolder | TeamRequests>
} else {
return {
status: "loaded",
data: [],
}
}
} }
}
const items = this.findCollInTree(this.data.value, parsedID)
if (items) {
const data = [
...(items.children
? items.children.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.children && items.children.length > 1
? index === items.children.length - 1
: false,
type: "folders",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
...(items.requests
? items.requests.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.requests && items.requests.length > 1
? index === items.requests.length - 1
: false,
type: "requests",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
]
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamFolder | TeamRequests>
}
return {
status: "loaded",
data: [],
} }
}) })
} }

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