chore: split app to commons and web (squash commit)

This commit is contained in:
Andrew Bastin
2022-12-02 02:57:46 -05:00
parent fb827e3586
commit 3d004f2322
535 changed files with 1487 additions and 501 deletions

View File

@@ -0,0 +1,73 @@
import { Subject, BehaviorSubject } from "rxjs"
import { map } from "rxjs/operators"
import { assign, clone } from "lodash-es"
type dispatcherFunc<StoreType> = (
currentVal: StoreType,
payload: any
) => Partial<StoreType>
/**
* Defines a dispatcher.
*
* This function exists to provide better typing for dispatch function.
* As you can see, its pretty much an identity function.
*/
export const defineDispatchers = <StoreType, T>(
// eslint-disable-next-line no-unused-vars
dispatchers: { [_ in keyof T]: dispatcherFunc<StoreType> }
) => dispatchers
type Dispatch<
StoreType,
DispatchersType extends Record<string, dispatcherFunc<StoreType>>
> = {
dispatcher: keyof DispatchersType
payload: any
}
export default class DispatchingStore<
StoreType,
DispatchersType extends Record<string, dispatcherFunc<StoreType>>
> {
#state$: BehaviorSubject<StoreType>
#dispatchers: DispatchersType
#dispatches$: Subject<Dispatch<StoreType, DispatchersType>> = new Subject()
constructor(initialValue: StoreType, dispatchers: DispatchersType) {
this.#state$ = new BehaviorSubject(initialValue)
this.#dispatchers = dispatchers
this.#dispatches$
.pipe(
map(({ dispatcher, payload }) =>
this.#dispatchers[dispatcher](this.value, payload)
)
)
.subscribe((val) => {
const data = clone(this.value)
assign(data, val)
this.#state$.next(data)
})
}
get subject$() {
return this.#state$
}
get value() {
return this.subject$.value
}
get dispatches$() {
return this.#dispatches$
}
dispatch({ dispatcher, payload }: Dispatch<StoreType, DispatchersType>) {
if (!this.#dispatchers[dispatcher])
throw new Error(`Undefined dispatch type '${dispatcher}'`)
this.#dispatches$.next({ dispatcher, payload })
}
}

View File

@@ -0,0 +1,281 @@
import { distinctUntilChanged, pluck } from "rxjs/operators"
import {
GQLHeader,
HoppGQLRequest,
makeGQLRequest,
HoppGQLAuth,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { useStream } from "@composables/stream"
type GQLSession = {
request: HoppGQLRequest
schema: string
response: string
}
export const defaultGQLSession: GQLSession = {
request: makeGQLRequest({
name: "Untitled request",
url: "https://echo.hoppscotch.io/graphql",
headers: [],
variables: `{
"id": "1"
}`,
query: `query Request {
method
url
headers {
key
value
}
}
`,
auth: {
authType: "none",
authActive: true,
},
}),
schema: "",
response: "",
}
const dispatchers = defineDispatchers({
setSession(_: GQLSession, { session }: { session: GQLSession }) {
return session
},
setName(curr: GQLSession, { newName }: { newName: string }) {
return {
request: {
...curr.request,
name: newName,
},
}
},
setURL(curr: GQLSession, { newURL }: { newURL: string }) {
return {
request: {
...curr.request,
url: newURL,
},
}
},
setHeaders(curr: GQLSession, { headers }: { headers: GQLHeader[] }) {
return {
request: {
...curr.request,
headers,
},
}
},
addHeader(curr: GQLSession, { header }: { header: GQLHeader }) {
return {
request: {
...curr.request,
headers: [...curr.request.headers, header],
},
}
},
removeHeader(curr: GQLSession, { headerIndex }: { headerIndex: number }) {
return {
request: {
...curr.request,
headers: curr.request.headers.filter((_x, i) => i !== headerIndex),
},
}
},
updateHeader(
curr: GQLSession,
{
headerIndex,
updatedHeader,
}: { headerIndex: number; updatedHeader: GQLHeader }
) {
return {
request: {
...curr.request,
headers: curr.request.headers.map((x, i) =>
i === headerIndex ? updatedHeader : x
),
},
}
},
setQuery(curr: GQLSession, { newQuery }: { newQuery: string }) {
return {
request: {
...curr.request,
query: newQuery,
},
}
},
setVariables(curr: GQLSession, { newVariables }: { newVariables: string }) {
return {
request: {
...curr.request,
variables: newVariables,
},
}
},
setResponse(_: GQLSession, { newResponse }: { newResponse: string }) {
return {
response: newResponse,
}
},
setAuth(curr: GQLSession, { newAuth }: { newAuth: HoppGQLAuth }) {
return {
request: {
...curr.request,
auth: newAuth,
},
}
},
})
export const gqlSessionStore = new DispatchingStore(
defaultGQLSession,
dispatchers
)
export function setGQLURL(newURL: string) {
gqlSessionStore.dispatch({
dispatcher: "setURL",
payload: {
newURL,
},
})
}
export function setGQLHeaders(headers: GQLHeader[]) {
gqlSessionStore.dispatch({
dispatcher: "setHeaders",
payload: {
headers,
},
})
}
export function addGQLHeader(header: GQLHeader) {
gqlSessionStore.dispatch({
dispatcher: "addHeader",
payload: {
header,
},
})
}
export function updateGQLHeader(headerIndex: number, updatedHeader: GQLHeader) {
gqlSessionStore.dispatch({
dispatcher: "updateHeader",
payload: {
headerIndex,
updatedHeader,
},
})
}
export function removeGQLHeader(headerIndex: number) {
gqlSessionStore.dispatch({
dispatcher: "removeHeader",
payload: {
headerIndex,
},
})
}
export function clearGQLHeaders() {
gqlSessionStore.dispatch({
dispatcher: "setHeaders",
payload: {
headers: [],
},
})
}
export function setGQLQuery(newQuery: string) {
gqlSessionStore.dispatch({
dispatcher: "setQuery",
payload: {
newQuery,
},
})
}
export function setGQLVariables(newVariables: string) {
gqlSessionStore.dispatch({
dispatcher: "setVariables",
payload: {
newVariables,
},
})
}
export function setGQLResponse(newResponse: string) {
gqlSessionStore.dispatch({
dispatcher: "setResponse",
payload: {
newResponse,
},
})
}
export function getGQLSession() {
return gqlSessionStore.value
}
export function setGQLSession(session: GQLSession) {
gqlSessionStore.dispatch({
dispatcher: "setSession",
payload: {
session,
},
})
}
export function useGQLRequestName() {
return useStream(gqlName$, gqlSessionStore.value.request.name, (newName) => {
gqlSessionStore.dispatch({
dispatcher: "setName",
payload: { newName },
})
})
}
export function setGQLAuth(newAuth: HoppGQLAuth) {
gqlSessionStore.dispatch({
dispatcher: "setAuth",
payload: {
newAuth,
},
})
}
export const gqlName$ = gqlSessionStore.subject$.pipe(
pluck("request", "name"),
distinctUntilChanged()
)
export const gqlURL$ = gqlSessionStore.subject$.pipe(
pluck("request", "url"),
distinctUntilChanged()
)
export const gqlQuery$ = gqlSessionStore.subject$.pipe(
pluck("request", "query"),
distinctUntilChanged()
)
export const gqlVariables$ = gqlSessionStore.subject$.pipe(
pluck("request", "variables"),
distinctUntilChanged()
)
export const gqlHeaders$ = gqlSessionStore.subject$.pipe(
pluck("request", "headers"),
distinctUntilChanged()
)
export const gqlResponse$ = gqlSessionStore.subject$.pipe(
pluck("response"),
distinctUntilChanged()
)
export const gqlAuth$ = gqlSessionStore.subject$.pipe(
pluck("request", "auth"),
distinctUntilChanged()
)

View File

@@ -0,0 +1,40 @@
import { distinctUntilChanged, pluck } from "rxjs"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
export type ExtensionStatus = "available" | "unknown-origin" | "waiting"
type InitialState = {
extensionStatus: ExtensionStatus
}
const initialState: InitialState = {
extensionStatus: "waiting",
}
const dispatchers = defineDispatchers({
changeExtensionStatus(
_,
{ extensionStatus }: { extensionStatus: ExtensionStatus }
) {
return {
extensionStatus,
}
},
})
export const hoppExtensionStore = new DispatchingStore(
initialState,
dispatchers
)
export const extensionStatus$ = hoppExtensionStore.subject$.pipe(
pluck("extensionStatus"),
distinctUntilChanged()
)
export function changeExtensionStatus(extensionStatus: ExtensionStatus) {
hoppExtensionStore.dispatch({
dispatcher: "changeExtensionStatus",
payload: { extensionStatus },
})
}

View File

@@ -0,0 +1,304 @@
import { distinctUntilChanged, pluck } from "rxjs/operators"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { MQTTConnection } from "~/helpers/realtime/MQTTConnection"
import {
HoppRealtimeLog,
HoppRealtimeLogLine,
} from "~/helpers/types/HoppRealtimeLog"
type MQTTTab = {
id: string
name: string
color: string
removable: boolean
logs: HoppRealtimeLog[]
}
type HoppMQTTRequest = {
endpoint: string
clientID: string
}
type HoppMQTTSession = {
request: HoppMQTTRequest
subscriptionState: boolean
log: HoppRealtimeLog
socket: MQTTConnection
tabs: MQTTTab[]
currentTabId: string
}
const defaultMQTTRequest: HoppMQTTRequest = {
endpoint: "wss://test.mosquitto.org:8081",
clientID: "hoppscotch",
}
const defaultTab: MQTTTab = {
id: "all",
name: "All Topics",
color: "var(--accent-color)",
removable: false,
logs: [],
}
const defaultMQTTSession: HoppMQTTSession = {
request: defaultMQTTRequest,
subscriptionState: false,
socket: new MQTTConnection(),
log: [],
tabs: [defaultTab],
currentTabId: defaultTab.id,
}
const dispatchers = defineDispatchers({
setRequest(
_: HoppMQTTSession,
{ newRequest }: { newRequest: HoppMQTTRequest }
) {
return {
request: newRequest,
}
},
setEndpoint(curr: HoppMQTTSession, { newEndpoint }: { newEndpoint: string }) {
return {
request: {
clientID: curr.request.clientID,
endpoint: newEndpoint,
},
}
},
setClientID(curr: HoppMQTTSession, { newClientID }: { newClientID: string }) {
return {
request: {
endpoint: curr.request.endpoint,
clientID: newClientID,
},
}
},
setConn(_: HoppMQTTSession, { socket }: { socket: MQTTConnection }) {
return {
socket,
}
},
setSubscriptionState(_: HoppMQTTSession, { state }: { state: boolean }) {
return {
subscriptionState: state,
}
},
setLog(_: HoppMQTTSession, { log }: { log: HoppRealtimeLog }) {
return {
log,
}
},
addLogLine(curr: HoppMQTTSession, { line }: { line: HoppRealtimeLogLine }) {
return {
log: [...curr.log, line],
}
},
setTabs(_: HoppMQTTSession, { tabs }: { tabs: MQTTTab[] }) {
return {
tabs,
}
},
addTab(curr: HoppMQTTSession, { tab }: { tab: MQTTTab }) {
return {
tabs: [...curr.tabs, tab],
}
},
setCurrentTabId(_: HoppMQTTSession, { tabId }: { tabId: string }) {
return {
currentTabId: tabId,
}
},
setCurrentTabLog(
_: HoppMQTTSession,
{ log, tabId }: { log: HoppRealtimeLog[]; tabId: string }
) {
const newTabs = _.tabs.map((tab) => {
if (tab.id === tabId) tab.logs = log
return tab
})
return {
tabs: newTabs,
}
},
addCurrentTabLogLine(
_: HoppMQTTSession,
{ line, tabId }: { tabId: string; line: HoppRealtimeLog }
) {
const newTabs = _.tabs.map((tab) => {
if (tab.id === tabId) tab.logs = [...tab.logs, line]
return tab
})
return {
tabs: newTabs,
}
},
})
const MQTTSessionStore = new DispatchingStore(defaultMQTTSession, dispatchers)
export function setMQTTRequest(newRequest?: HoppMQTTRequest) {
MQTTSessionStore.dispatch({
dispatcher: "setRequest",
payload: {
newRequest: newRequest ?? defaultMQTTRequest,
},
})
}
export function setMQTTEndpoint(newEndpoint: string) {
MQTTSessionStore.dispatch({
dispatcher: "setEndpoint",
payload: {
newEndpoint,
},
})
}
export function setMQTTClientID(newClientID: string) {
MQTTSessionStore.dispatch({
dispatcher: "setClientID",
payload: {
newClientID,
},
})
}
export function setMQTTConn(socket: MQTTConnection) {
MQTTSessionStore.dispatch({
dispatcher: "setConn",
payload: {
socket,
},
})
}
export function setMQTTSubscriptionState(state: boolean) {
MQTTSessionStore.dispatch({
dispatcher: "setSubscriptionState",
payload: {
state,
},
})
}
export function setMQTTLog(log: HoppRealtimeLog) {
MQTTSessionStore.dispatch({
dispatcher: "setLog",
payload: {
log,
},
})
}
export function addMQTTLogLine(line: HoppRealtimeLogLine) {
MQTTSessionStore.dispatch({
dispatcher: "addLogLine",
payload: {
line,
},
})
}
export function setMQTTTabs(tabs: MQTTTab[]) {
MQTTSessionStore.dispatch({
dispatcher: "setTabs",
payload: {
tabs,
},
})
}
export function addMQTTTab(tab: MQTTTab) {
MQTTSessionStore.dispatch({
dispatcher: "addTab",
payload: {
tab,
},
})
}
export function setCurrentTab(tabId: string) {
MQTTSessionStore.dispatch({
dispatcher: "setCurrentTabId",
payload: {
tabId,
},
})
}
export function setMQTTCurrentTabLog(tabId: string, log: HoppRealtimeLog) {
MQTTSessionStore.dispatch({
dispatcher: "setCurrentTabLog",
payload: {
tabId,
log,
},
})
}
export function addMQTTCurrentTabLogLine(
tabId: string,
line: HoppRealtimeLogLine
) {
MQTTSessionStore.dispatch({
dispatcher: "addCurrentTabLogLine",
payload: {
tabId,
line,
},
})
}
export const MQTTRequest$ = MQTTSessionStore.subject$.pipe(
pluck("request"),
distinctUntilChanged()
)
export const MQTTEndpoint$ = MQTTSessionStore.subject$.pipe(
pluck("request", "endpoint"),
distinctUntilChanged()
)
export const MQTTClientID$ = MQTTSessionStore.subject$.pipe(
pluck("request", "clientID"),
distinctUntilChanged()
)
export const MQTTConnectingState$ = MQTTSessionStore.subject$.pipe(
pluck("connectingState"),
distinctUntilChanged()
)
export const MQTTConnectionState$ = MQTTSessionStore.subject$.pipe(
pluck("connectionState"),
distinctUntilChanged()
)
export const MQTTSubscriptionState$ = MQTTSessionStore.subject$.pipe(
pluck("subscriptionState"),
distinctUntilChanged()
)
export const MQTTConn$ = MQTTSessionStore.subject$.pipe(
pluck("socket"),
distinctUntilChanged()
)
export const MQTTLog$ = MQTTSessionStore.subject$.pipe(
pluck("log"),
distinctUntilChanged()
)
export const MQTTTabs$ = MQTTSessionStore.subject$.pipe(
pluck("tabs"),
distinctUntilChanged()
)
export const MQTTCurrentTab$ = MQTTSessionStore.subject$.pipe(
pluck("currentTabId"),
distinctUntilChanged()
)

View File

@@ -0,0 +1,710 @@
import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
import { Ref } from "vue"
import {
FormDataKeyValue,
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
RESTReqSchemaVersion,
HoppRESTAuth,
ValidContentTypes,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useStream } from "@composables/stream"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
type RESTSession = {
request: HoppRESTRequest
response: HoppRESTResponse | null
testResults: HoppTestResult | null
saveContext: HoppRequestSaveContext | null
}
export const getDefaultRESTRequest = (): HoppRESTRequest => ({
v: RESTReqSchemaVersion,
endpoint: "https://echo.hoppscotch.io",
name: "Untitled request",
params: [],
headers: [],
method: "GET",
auth: {
authType: "none",
authActive: true,
},
preRequestScript: "",
testScript: "",
body: {
contentType: null,
body: null,
},
})
const defaultRESTSession: RESTSession = {
request: getDefaultRESTRequest(),
response: null,
testResults: null,
saveContext: null,
}
const dispatchers = defineDispatchers({
setRequest(_: RESTSession, { req }: { req: HoppRESTRequest }) {
return {
request: req,
}
},
setRequestName(curr: RESTSession, { newName }: { newName: string }) {
return {
request: {
...curr.request,
name: newName,
},
}
},
setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) {
return {
request: {
...curr.request,
endpoint: newEndpoint,
},
}
},
setParams(curr: RESTSession, { entries }: { entries: HoppRESTParam[] }) {
return {
request: {
...curr.request,
params: entries,
},
}
},
addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
return {
request: {
...curr.request,
params: [...curr.request.params, newParam],
},
}
},
updateParam(
curr: RESTSession,
{ index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
) {
const newParams = curr.request.params.map((param, i) => {
if (i === index) return updatedParam
else return param
})
return {
request: {
...curr.request,
params: newParams,
},
}
},
deleteParam(curr: RESTSession, { index }: { index: number }) {
const newParams = curr.request.params.filter((_x, i) => i !== index)
return {
request: {
...curr.request,
params: newParams,
},
}
},
deleteAllParams(curr: RESTSession) {
return {
request: {
...curr.request,
params: [],
},
}
},
updateMethod(curr: RESTSession, { newMethod }: { newMethod: string }) {
return {
request: {
...curr.request,
method: newMethod,
},
}
},
setHeaders(curr: RESTSession, { entries }: { entries: HoppRESTHeader[] }) {
return {
request: {
...curr.request,
headers: entries,
},
}
},
addHeader(curr: RESTSession, { entry }: { entry: HoppRESTHeader }) {
return {
request: {
...curr.request,
headers: [...curr.request.headers, entry],
},
}
},
updateHeader(
curr: RESTSession,
{ index, updatedEntry }: { index: number; updatedEntry: HoppRESTHeader }
) {
return {
request: {
...curr.request,
headers: curr.request.headers.map((header, i) => {
if (i === index) return updatedEntry
else return header
}),
},
}
},
deleteHeader(curr: RESTSession, { index }: { index: number }) {
return {
request: {
...curr.request,
headers: curr.request.headers.filter((_, i) => i !== index),
},
}
},
deleteAllHeaders(curr: RESTSession) {
return {
request: {
...curr.request,
headers: [],
},
}
},
setAuth(curr: RESTSession, { newAuth }: { newAuth: HoppRESTAuth }) {
return {
request: {
...curr.request,
auth: newAuth,
},
}
},
setPreRequestScript(curr: RESTSession, { newScript }: { newScript: string }) {
return {
request: {
...curr.request,
preRequestScript: newScript,
},
}
},
setTestScript(curr: RESTSession, { newScript }: { newScript: string }) {
return {
request: {
...curr.request,
testScript: newScript,
},
}
},
setContentType(
curr: RESTSession,
{ newContentType }: { newContentType: ValidContentTypes | null }
) {
// TODO: persist body evenafter switching content typees
return {
request: {
...curr.request,
body: applyBodyTransition(curr.request.body, newContentType),
},
}
},
addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: [...curr.request.body.body, entry],
},
},
}
},
deleteFormDataEntry(curr: RESTSession, { index }: { index: number }) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: curr.request.body.body.filter((_, i) => i !== index),
},
},
}
},
updateFormDataEntry(
curr: RESTSession,
{ index, entry }: { index: number; entry: FormDataKeyValue }
) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: curr.request.body.body.map((x, i) => (i !== index ? x : entry)),
},
},
}
},
deleteAllFormDataEntries(curr: RESTSession) {
// Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {}
return {
request: {
...curr.request,
body: <HoppRESTReqBody>{
contentType: "multipart/form-data",
body: [],
},
},
}
},
setRequestBody(curr: RESTSession, { newBody }: { newBody: HoppRESTReqBody }) {
return {
request: {
...curr.request,
body: newBody,
},
}
},
updateResponse(
_curr: RESTSession,
{ updatedRes }: { updatedRes: HoppRESTResponse | null }
) {
return {
response: updatedRes,
}
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clearResponse(_curr: RESTSession) {
return {
response: null,
}
},
setTestResults(
_curr: RESTSession,
{ newResults }: { newResults: HoppTestResult | null }
) {
return {
testResults: newResults,
}
},
setSaveContext(
_,
{ newContext }: { newContext: HoppRequestSaveContext | null }
) {
return {
saveContext: newContext,
}
},
})
const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers)
export function getRESTRequest() {
return restSessionStore.subject$.value.request
}
export function setRESTRequest(
req: HoppRESTRequest,
saveContext?: HoppRequestSaveContext | null
) {
restSessionStore.dispatch({
dispatcher: "setRequest",
payload: {
req,
},
})
if (saveContext) setRESTSaveContext(saveContext)
}
export function setRESTSaveContext(saveContext: HoppRequestSaveContext | null) {
restSessionStore.dispatch({
dispatcher: "setSaveContext",
payload: {
newContext: saveContext,
},
})
}
export function getRESTSaveContext() {
return restSessionStore.value.saveContext
}
export function resetRESTRequest() {
setRESTRequest(getDefaultRESTRequest())
}
export function setRESTEndpoint(newEndpoint: string) {
restSessionStore.dispatch({
dispatcher: "setEndpoint",
payload: {
newEndpoint,
},
})
}
export function setRESTRequestName(newName: string) {
restSessionStore.dispatch({
dispatcher: "setRequestName",
payload: {
newName,
},
})
}
export function setRESTParams(entries: HoppRESTParam[]) {
restSessionStore.dispatch({
dispatcher: "setParams",
payload: {
entries,
},
})
}
export function addRESTParam(newParam: HoppRESTParam) {
restSessionStore.dispatch({
dispatcher: "addParam",
payload: {
newParam,
},
})
}
export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
restSessionStore.dispatch({
dispatcher: "updateParam",
payload: {
updatedParam,
index,
},
})
}
export function deleteRESTParam(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteParam",
payload: {
index,
},
})
}
export function deleteAllRESTParams() {
restSessionStore.dispatch({
dispatcher: "deleteAllParams",
payload: {},
})
}
export function updateRESTMethod(newMethod: string) {
restSessionStore.dispatch({
dispatcher: "updateMethod",
payload: {
newMethod,
},
})
}
export function setRESTHeaders(entries: HoppRESTHeader[]) {
restSessionStore.dispatch({
dispatcher: "setHeaders",
payload: {
entries,
},
})
}
export function addRESTHeader(entry: HoppRESTHeader) {
restSessionStore.dispatch({
dispatcher: "addHeader",
payload: {
entry,
},
})
}
export function updateRESTHeader(index: number, updatedEntry: HoppRESTHeader) {
restSessionStore.dispatch({
dispatcher: "updateHeader",
payload: {
index,
updatedEntry,
},
})
}
export function deleteRESTHeader(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteHeader",
payload: {
index,
},
})
}
export function deleteAllRESTHeaders() {
restSessionStore.dispatch({
dispatcher: "deleteAllHeaders",
payload: {},
})
}
export function setRESTAuth(newAuth: HoppRESTAuth) {
restSessionStore.dispatch({
dispatcher: "setAuth",
payload: {
newAuth,
},
})
}
export function setRESTPreRequestScript(newScript: string) {
restSessionStore.dispatch({
dispatcher: "setPreRequestScript",
payload: {
newScript,
},
})
}
export function setRESTTestScript(newScript: string) {
restSessionStore.dispatch({
dispatcher: "setTestScript",
payload: {
newScript,
},
})
}
export function setRESTReqBody(newBody: HoppRESTReqBody | null) {
restSessionStore.dispatch({
dispatcher: "setRequestBody",
payload: {
newBody,
},
})
}
export function updateRESTResponse(updatedRes: HoppRESTResponse | null) {
restSessionStore.dispatch({
dispatcher: "updateResponse",
payload: {
updatedRes,
},
})
}
export function clearRESTResponse() {
restSessionStore.dispatch({
dispatcher: "clearResponse",
payload: {},
})
}
export function setRESTTestResults(newResults: HoppTestResult | null) {
restSessionStore.dispatch({
dispatcher: "setTestResults",
payload: {
newResults,
},
})
}
export function addFormDataEntry(entry: FormDataKeyValue) {
restSessionStore.dispatch({
dispatcher: "addFormDataEntry",
payload: {
entry,
},
})
}
export function deleteFormDataEntry(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteFormDataEntry",
payload: {
index,
},
})
}
export function updateFormDataEntry(index: number, entry: FormDataKeyValue) {
restSessionStore.dispatch({
dispatcher: "updateFormDataEntry",
payload: {
index,
entry,
},
})
}
export function setRESTContentType(newContentType: ValidContentTypes | null) {
restSessionStore.dispatch({
dispatcher: "setContentType",
payload: {
newContentType,
},
})
}
export function deleteAllFormDataEntries() {
restSessionStore.dispatch({
dispatcher: "deleteAllFormDataEntries",
payload: {},
})
}
export const restSaveContext$ = restSessionStore.subject$.pipe(
pluck("saveContext"),
distinctUntilChanged()
)
export const restRequest$ = restSessionStore.subject$.pipe(
pluck("request"),
distinctUntilChanged()
)
export const restRequestName$ = restRequest$.pipe(
pluck("name"),
distinctUntilChanged()
)
export const restEndpoint$ = restSessionStore.subject$.pipe(
pluck("request", "endpoint"),
distinctUntilChanged()
)
export const restParams$ = restSessionStore.subject$.pipe(
pluck("request", "params"),
distinctUntilChanged()
)
export const restActiveParamsCount$ = restParams$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
)
)
export const restMethod$ = restSessionStore.subject$.pipe(
pluck("request", "method"),
distinctUntilChanged()
)
export const restHeaders$ = restSessionStore.subject$.pipe(
pluck("request", "headers"),
distinctUntilChanged()
)
export const restActiveHeadersCount$ = restHeaders$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
)
)
export const restAuth$ = restRequest$.pipe(pluck("auth"))
export const restPreRequestScript$ = restSessionStore.subject$.pipe(
pluck("request", "preRequestScript"),
distinctUntilChanged()
)
export const restContentType$ = restRequest$.pipe(
pluck("body", "contentType"),
distinctUntilChanged()
)
export const restTestScript$ = restSessionStore.subject$.pipe(
pluck("request", "testScript"),
distinctUntilChanged()
)
export const restReqBody$ = restSessionStore.subject$.pipe(
pluck("request", "body"),
distinctUntilChanged()
)
export const restResponse$ = restSessionStore.subject$.pipe(
pluck("response"),
distinctUntilChanged()
)
export const completedRESTResponse$ = restResponse$.pipe(
filter(
(res) =>
res !== null &&
res.type !== "loading" &&
res.type !== "network_fail" &&
res.type !== "script_fail"
)
)
export const restTestResults$ = restSessionStore.subject$.pipe(
pluck("testResults"),
distinctUntilChanged()
)
/**
* A Vue 3 composable function that gives access to a ref
* which is updated to the preRequestScript value in the store.
* The ref value is kept in sync with the store and all writes
* to the ref are dispatched to the store as `setPreRequestScript`
* dispatches.
*/
export function usePreRequestScript(): Ref<string> {
return useStream(
restPreRequestScript$,
restSessionStore.value.request.preRequestScript,
(value) => {
setRESTPreRequestScript(value)
}
)
}
/**
* A Vue 3 composable function that gives access to a ref
* which is updated to the testScript value in the store.
* The ref value is kept in sync with the store and all writes
* to the ref are dispatched to the store as `setTestScript`
* dispatches.
*/
export function useTestScript(): Ref<string> {
return useStream(
restTestScript$,
restSessionStore.value.request.testScript,
(value) => {
setRESTTestScript(value)
}
)
}
export function useRESTRequestBody(): Ref<HoppRESTReqBody> {
return useStream(
restReqBody$,
restSessionStore.value.request.body,
setRESTReqBody
)
}
export function useRESTRequestName(): Ref<string> {
return useStream(
restRequestName$,
restSessionStore.value.request.name,
setRESTRequestName
)
}

View File

@@ -0,0 +1,157 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import {
HoppRealtimeLog,
HoppRealtimeLogLine,
} from "~/helpers/types/HoppRealtimeLog"
import { SSEConnection } from "~/helpers/realtime/SSEConnection"
type HoppSSERequest = {
endpoint: string
eventType: string
}
type HoppSSESession = {
request: HoppSSERequest
log: HoppRealtimeLog
socket: SSEConnection
}
const defaultSSERequest: HoppSSERequest = {
endpoint: "https://express-eventsource.herokuapp.com/events",
eventType: "data",
}
const defaultSSESession: HoppSSESession = {
request: defaultSSERequest,
socket: new SSEConnection(),
log: [],
}
const dispatchers = defineDispatchers({
setRequest(
_: HoppSSESession,
{ newRequest }: { newRequest: HoppSSERequest }
) {
return {
request: newRequest,
}
},
setEndpoint(curr: HoppSSESession, { newEndpoint }: { newEndpoint: string }) {
return {
request: {
eventType: curr.request.eventType,
endpoint: newEndpoint,
},
}
},
setEventType(curr: HoppSSESession, { newType }: { newType: string }) {
return {
request: {
endpoint: curr.request.endpoint,
eventType: newType,
},
}
},
setSocket(_: HoppSSESession, { socket }: { socket: SSEConnection }) {
return {
socket,
}
},
setLog(_: HoppSSESession, { log }: { log: HoppRealtimeLog }) {
return {
log,
}
},
addLogLine(curr: HoppSSESession, { line }: { line: HoppRealtimeLogLine }) {
return {
log: [...curr.log, line],
}
},
})
const SSESessionStore = new DispatchingStore(defaultSSESession, dispatchers)
export function setSSERequest(newRequest?: HoppSSERequest) {
SSESessionStore.dispatch({
dispatcher: "setRequest",
payload: {
newRequest: newRequest ?? defaultSSERequest,
},
})
}
export function setSSEEndpoint(newEndpoint: string) {
SSESessionStore.dispatch({
dispatcher: "setEndpoint",
payload: {
newEndpoint,
},
})
}
export function setSSEEventType(newType: string) {
SSESessionStore.dispatch({
dispatcher: "setEventType",
payload: {
newType,
},
})
}
export function setSSESocket(socket: SSEConnection) {
SSESessionStore.dispatch({
dispatcher: "setSocket",
payload: {
socket,
},
})
}
export function setSSELog(log: HoppRealtimeLog) {
SSESessionStore.dispatch({
dispatcher: "setLog",
payload: {
log,
},
})
}
export function addSSELogLine(line: HoppRealtimeLogLine) {
SSESessionStore.dispatch({
dispatcher: "addLogLine",
payload: {
line,
},
})
}
export const SSERequest$ = SSESessionStore.subject$.pipe(
pluck("request"),
distinctUntilChanged()
)
export const SSEEndpoint$ = SSESessionStore.subject$.pipe(
pluck("request", "endpoint"),
distinctUntilChanged()
)
export const SSEEventType$ = SSESessionStore.subject$.pipe(
pluck("request", "eventType"),
distinctUntilChanged()
)
export const SSEConnectingState$ = SSESessionStore.subject$.pipe(
pluck("connectingState"),
distinctUntilChanged()
)
export const SSESocket$ = SSESessionStore.subject$.pipe(
pluck("socket"),
distinctUntilChanged()
)
export const SSELog$ = SSESessionStore.subject$.pipe(
pluck("log"),
distinctUntilChanged()
)

View File

@@ -0,0 +1,190 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import { Socket as SocketV2 } from "socket.io-client-v2"
import { Socket as SocketV3 } from "socket.io-client-v3"
import { Socket as SocketV4 } from "socket.io-client-v4"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import {
HoppRealtimeLog,
HoppRealtimeLogLine,
} from "~/helpers/types/HoppRealtimeLog"
type SocketIO = SocketV2 | SocketV3 | SocketV4
export type SIOClientVersion = "v4" | "v3" | "v2"
type HoppSIORequest = {
endpoint: string
path: string
version: SIOClientVersion
}
type HoppSIOSession = {
request: HoppSIORequest
log: HoppRealtimeLog
socket: SocketIO | null
}
const defaultSIORequest: HoppSIORequest = {
endpoint: "wss://echo-socketio.hoppscotch.io",
path: "/socket.io",
version: "v4",
}
const defaultSIOSession: HoppSIOSession = {
request: defaultSIORequest,
socket: null,
log: [],
}
const dispatchers = defineDispatchers({
setRequest(
_: HoppSIOSession,
{ newRequest }: { newRequest: HoppSIORequest }
) {
return {
request: newRequest,
}
},
setEndpoint(curr: HoppSIOSession, { newEndpoint }: { newEndpoint: string }) {
return {
request: {
...curr.request,
endpoint: newEndpoint,
},
}
},
setPath(curr: HoppSIOSession, { newPath }: { newPath: string }) {
return {
request: {
...curr.request,
path: newPath,
},
}
},
setVersion(
curr: HoppSIOSession,
{ newVersion }: { newVersion: SIOClientVersion }
) {
return {
request: {
...curr.request,
version: newVersion,
},
}
},
setSocket(_: HoppSIOSession, { socket }: { socket: SocketIO }) {
return {
socket,
}
},
setLog(_: HoppSIOSession, { log }: { log: HoppRealtimeLog }) {
return {
log,
}
},
addLogLine(curr: HoppSIOSession, { line }: { line: HoppRealtimeLogLine }) {
return {
log: [...curr.log, line],
}
},
})
const SIOSessionStore = new DispatchingStore(defaultSIOSession, dispatchers)
export function setSIORequest(newRequest?: HoppSIORequest) {
SIOSessionStore.dispatch({
dispatcher: "setRequest",
payload: {
newRequest: newRequest ?? defaultSIORequest,
},
})
}
export function setSIOEndpoint(newEndpoint: string) {
SIOSessionStore.dispatch({
dispatcher: "setEndpoint",
payload: {
newEndpoint,
},
})
}
export function setSIOVersion(newVersion: string) {
SIOSessionStore.dispatch({
dispatcher: "setVersion",
payload: {
newVersion,
},
})
}
export function setSIOPath(newPath: string) {
SIOSessionStore.dispatch({
dispatcher: "setPath",
payload: {
newPath,
},
})
}
export function setSIOSocket(socket: SocketIO) {
SIOSessionStore.dispatch({
dispatcher: "setSocket",
payload: {
socket,
},
})
}
export function setSIOLog(log: HoppRealtimeLog) {
SIOSessionStore.dispatch({
dispatcher: "setLog",
payload: {
log,
},
})
}
export function addSIOLogLine(line: HoppRealtimeLogLine) {
SIOSessionStore.dispatch({
dispatcher: "addLogLine",
payload: {
line,
},
})
}
export const SIORequest$ = SIOSessionStore.subject$.pipe(
pluck("request"),
distinctUntilChanged()
)
export const SIOEndpoint$ = SIOSessionStore.subject$.pipe(
pluck("request", "endpoint"),
distinctUntilChanged()
)
export const SIOVersion$ = SIOSessionStore.subject$.pipe(
pluck("request", "version"),
distinctUntilChanged()
)
export const SIOPath$ = SIOSessionStore.subject$.pipe(
pluck("request", "path"),
distinctUntilChanged()
)
export const SIOConnectionState$ = SIOSessionStore.subject$.pipe(
pluck("connectionState"),
distinctUntilChanged()
)
export const SIOSocket$ = SIOSessionStore.subject$.pipe(
pluck("socket"),
distinctUntilChanged()
)
export const SIOLog$ = SIOSessionStore.subject$.pipe(
pluck("log"),
distinctUntilChanged()
)

View File

@@ -0,0 +1,245 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import {
HoppRealtimeLog,
HoppRealtimeLogLine,
} from "~/helpers/types/HoppRealtimeLog"
import { WSConnection } from "~/helpers/realtime/WSConnection"
export type HoppWSProtocol = {
value: string
active: boolean
}
type HoppWSRequest = {
endpoint: string
protocols: HoppWSProtocol[]
}
export type HoppWSSession = {
request: HoppWSRequest
log: HoppRealtimeLog
socket: WSConnection
}
const defaultWSRequest: HoppWSRequest = {
endpoint: "wss://echo-websocket.hoppscotch.io",
protocols: [],
}
const defaultWSSession: HoppWSSession = {
request: defaultWSRequest,
socket: new WSConnection(),
log: [],
}
const dispatchers = defineDispatchers({
setRequest(_: HoppWSSession, { newRequest }: { newRequest: HoppWSRequest }) {
return {
request: newRequest,
}
},
setEndpoint(curr: HoppWSSession, { newEndpoint }: { newEndpoint: string }) {
return {
request: {
protocols: curr.request.protocols,
endpoint: newEndpoint,
},
}
},
setProtocols(
curr: HoppWSSession,
{ protocols }: { protocols: HoppWSProtocol[] }
) {
return {
request: {
protocols,
endpoint: curr.request.endpoint,
},
}
},
addProtocol(curr: HoppWSSession, { protocol }: { protocol: HoppWSProtocol }) {
return {
request: {
endpoint: curr.request.endpoint,
protocols: [...curr.request.protocols, protocol],
},
}
},
deleteProtocol(curr: HoppWSSession, { index }: { index: number }) {
return {
request: {
endpoint: curr.request.endpoint,
protocols: curr.request.protocols.filter((_, idx) => index !== idx),
},
}
},
deleteAllProtocols(curr: HoppWSSession) {
return {
request: {
endpoint: curr.request.endpoint,
protocols: [],
},
}
},
updateProtocol(
curr: HoppWSSession,
{
index,
updatedProtocol,
}: { index: number; updatedProtocol: HoppWSProtocol }
) {
return {
request: {
endpoint: curr.request.endpoint,
protocols: curr.request.protocols.map((proto, idx) => {
return index === idx ? updatedProtocol : proto
}),
},
}
},
setSocket(_: HoppWSSession, { socket }: { socket: WSConnection }) {
return {
socket,
}
},
setLog(_: HoppWSSession, { log }: { log: HoppRealtimeLog }) {
return {
log,
}
},
addLogLine(curr: HoppWSSession, { line }: { line: HoppRealtimeLogLine }) {
return {
log: [...curr.log, line],
}
},
})
const WSSessionStore = new DispatchingStore(defaultWSSession, dispatchers)
export function setWSRequest(newRequest?: HoppWSRequest) {
WSSessionStore.dispatch({
dispatcher: "setRequest",
payload: {
newRequest: newRequest ?? defaultWSRequest,
},
})
}
export function setWSEndpoint(newEndpoint: string) {
WSSessionStore.dispatch({
dispatcher: "setEndpoint",
payload: {
newEndpoint,
},
})
}
export function setWSProtocols(protocols: HoppWSProtocol[]) {
WSSessionStore.dispatch({
dispatcher: "setProtocols",
payload: {
protocols,
},
})
}
export function addWSProtocol(protocol: HoppWSProtocol) {
WSSessionStore.dispatch({
dispatcher: "addProtocol",
payload: {
protocol,
},
})
}
export function deleteWSProtocol(index: number) {
WSSessionStore.dispatch({
dispatcher: "deleteProtocol",
payload: {
index,
},
})
}
export function deleteAllWSProtocols() {
WSSessionStore.dispatch({
dispatcher: "deleteAllProtocols",
payload: {},
})
}
export function updateWSProtocol(
index: number,
updatedProtocol: HoppWSProtocol
) {
WSSessionStore.dispatch({
dispatcher: "updateProtocol",
payload: {
index,
updatedProtocol,
},
})
}
export function setWSSocket(socket: WSConnection) {
WSSessionStore.dispatch({
dispatcher: "setSocket",
payload: {
socket,
},
})
}
export function setWSLog(log: HoppRealtimeLog) {
WSSessionStore.dispatch({
dispatcher: "setLog",
payload: {
log,
},
})
}
export function addWSLogLine(line: HoppRealtimeLogLine) {
WSSessionStore.dispatch({
dispatcher: "addLogLine",
payload: {
line,
},
})
}
export const WSRequest$ = WSSessionStore.subject$.pipe(
pluck("request"),
distinctUntilChanged()
)
export const WSEndpoint$ = WSSessionStore.subject$.pipe(
pluck("request", "endpoint"),
distinctUntilChanged()
)
export const WSProtocols$ = WSSessionStore.subject$.pipe(
pluck("request", "protocols"),
distinctUntilChanged()
)
export const WSConnectingState$ = WSSessionStore.subject$.pipe(
pluck("connectingState"),
distinctUntilChanged()
)
export const WSConnectionState$ = WSSessionStore.subject$.pipe(
pluck("connectionState"),
distinctUntilChanged()
)
export const WSSocket$ = WSSessionStore.subject$.pipe(
pluck("socket"),
distinctUntilChanged()
)
export const WSLog$ = WSSessionStore.subject$.pipe(
pluck("log"),
distinctUntilChanged()
)

View File

@@ -0,0 +1,193 @@
import { BehaviorSubject, Subject } from "rxjs"
import { isEqual } from "lodash-es"
import DispatchingStore from "~/newstore/DispatchingStore"
describe("DispatchingStore", () => {
test("'subject$' property properly returns an BehaviorSubject", () => {
const store = new DispatchingStore({}, {})
expect(store.subject$ instanceof BehaviorSubject).toEqual(true)
})
test("'value' property properly returns the current state value", () => {
const store = new DispatchingStore({}, {})
expect(store.value).toEqual({})
})
test("'dispatches$' property properly returns a Subject", () => {
const store = new DispatchingStore({}, {})
expect(store.dispatches$ instanceof Subject).toEqual(true)
})
test("dispatch with invalid dispatcher are thrown", () => {
const store = new DispatchingStore({}, {})
expect(() => {
store.dispatch({
dispatcher: "non-existent",
payload: {},
})
}).toThrow()
})
test("valid dispatcher calls run without throwing", () => {
const store = new DispatchingStore(
{},
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
testDispatcher(_currentValue, _payload) {
// Nothing here
},
}
)
expect(() => {
store.dispatch({
dispatcher: "testDispatcher",
payload: {},
})
}).not.toThrow()
})
test("only correct dispatcher method is ran", () => {
const dispatchFn = jest.fn().mockReturnValue({})
const dontCallDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore(
{},
{
testDispatcher: dispatchFn,
dontCallDispatcher: dontCallDispatchFn,
}
)
store.dispatch({
dispatcher: "testDispatcher",
payload: {},
})
expect(dispatchFn).toHaveBeenCalledTimes(1)
expect(dontCallDispatchFn).not.toHaveBeenCalled()
})
test("passes current value and the payload to the dispatcher", () => {
const testInitValue = { name: "bob" }
const testPayload = { name: "alice" }
const testDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn,
})
store.dispatch({
dispatcher: "testDispatcher",
payload: testPayload,
})
expect(testDispatchFn).toHaveBeenCalledWith(testInitValue, testPayload)
})
test("dispatcher returns are used to update the store correctly", () => {
const testInitValue = { name: "bob" }
const testDispatchReturnVal = { name: "alice" }
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn,
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {}, // Payload doesn't matter because the function is mocked
})
expect(store.value).toEqual(testDispatchReturnVal)
})
test("dispatching patches in new values if not existing on the store", () => {
const testInitValue = { name: "bob" }
const testDispatchReturnVal = { age: 25 }
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn,
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {},
})
expect(store.value).toEqual({
name: "bob",
age: 25,
})
})
test("emits the current store value to the new subscribers", (done) => {
const testInitValue = { name: "bob" }
const testDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn,
})
store.subject$.subscribe((value) => {
if (value === testInitValue) {
done()
}
})
})
test("emits the dispatched store value to the subscribers", (done) => {
const testInitValue = { name: "bob" }
const testDispatchReturnVal = { age: 25 }
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn,
})
store.subject$.subscribe((value) => {
if (isEqual(value, { name: "bob", age: 25 })) {
done()
}
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {},
})
})
test("dispatching emits the new dispatch requests to the subscribers", () => {
const testInitValue = { name: "bob" }
const testPayload = { age: 25 }
const testDispatchFn = jest.fn().mockReturnValue({})
const store = new DispatchingStore(testInitValue, {
testDispatcher: testDispatchFn,
})
store.dispatches$.subscribe((value) => {
if (
isEqual(value, { dispatcher: "testDispatcher", payload: testPayload })
) {
done()
}
})
store.dispatch({
dispatcher: "testDispatcher",
payload: {},
})
})
})

View File

@@ -0,0 +1,889 @@
import { pluck } from "rxjs/operators"
import {
HoppGQLRequest,
HoppRESTRequest,
HoppCollection,
makeCollection,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { getRESTSaveContext, setRESTSaveContext } from "./RESTSession"
const defaultRESTCollectionState = {
state: [
makeCollection<HoppRESTRequest>({
name: "My Collection",
folders: [],
requests: [],
}),
],
}
const defaultGraphqlCollectionState = {
state: [
makeCollection<HoppGQLRequest>({
name: "My GraphQL Collection",
folders: [],
requests: [],
}),
],
}
type RESTCollectionStoreType = typeof defaultRESTCollectionState
type GraphqlCollectionStoreType = typeof defaultGraphqlCollectionState
function navigateToFolderWithIndexPath(
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
indexPaths: number[]
) {
if (indexPaths.length === 0) return null
let target = collections[indexPaths.shift() as number]
while (indexPaths.length > 0)
target = target.folders[indexPaths.shift() as number]
return target !== undefined ? target : null
}
const restCollectionDispatchers = defineDispatchers({
setCollections(
_: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
) {
return {
state: entries,
}
},
appendCollections(
{ state }: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
) {
return {
state: [...state, ...entries],
}
},
addCollection(
{ state }: RESTCollectionStoreType,
{ collection }: { collection: HoppCollection<any> }
) {
return {
state: [...state, collection],
}
},
removeCollection(
{ state }: RESTCollectionStoreType,
{ collectionIndex }: { collectionIndex: number }
) {
return {
state: (state as any).filter(
(_: any, i: number) => i !== collectionIndex
),
}
},
editCollection(
{ state }: RESTCollectionStoreType,
{
collectionIndex,
collection,
}: { collectionIndex: number; collection: HoppCollection<any> }
) {
return {
state: state.map((col, index) =>
index === collectionIndex ? collection : col
),
}
},
addFolder(
{ state }: RESTCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection<HoppRESTRequest> = makeCollection({
name,
folders: [],
requests: [],
})
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(`Could not parse path '${path}'. Ignoring add folder request`)
return {}
}
target.folders.push(newFolder)
return {
state: newState,
}
},
editFolder(
{ state }: RESTCollectionStoreType,
{ path, folder }: { path: string; folder: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(
`Could not parse path '${path}'. Ignoring edit folder request`
)
return {}
}
Object.assign(target, folder)
return {
state: newState,
}
},
removeFolder({ state }: RESTCollectionStoreType, { path }: { path: string }) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log(
"Given path too short. If this is a collection, use removeCollection dispatcher instead. Skipping request."
)
return {}
}
// We get the index path to the folder itself,
// we have to find the folder containing the target folder,
// so we pop the last path index
const folderIndex = indexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(newState, indexPaths)
if (containingFolder === null) {
console.log(
`Could not resolve path '${path}'. Skipping removeFolder dispatch.`
)
return {}
}
containingFolder.folders.splice(folderIndex, 1)
return {
state: newState,
}
},
editRequest(
{ state }: RESTCollectionStoreType,
{
path,
requestIndex,
requestNew,
}: { path: string; requestIndex: number; requestNew: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring editRequest dispatch.`
)
return {}
}
targetLocation.requests = targetLocation.requests.map((req, index) =>
index !== requestIndex ? req : requestNew
)
return {
state: newState,
}
},
saveRequestAs(
{ state }: RESTCollectionStoreType,
{ path, request }: { path: string; request: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring saveRequestAs dispatch.`
)
return {}
}
targetLocation.requests.push(request)
return {
state: newState,
}
},
removeRequest(
{ state }: RESTCollectionStoreType,
{ path, requestIndex }: { path: string; requestIndex: number }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring removeRequest dispatch.`
)
return {}
}
targetLocation.requests.splice(requestIndex, 1)
// If the save context is set and is set to the same source, we invalidate it
const saveCtx = getRESTSaveContext()
if (
saveCtx?.originLocation === "user-collection" &&
saveCtx.folderPath === path &&
saveCtx.requestIndex === requestIndex
) {
setRESTSaveContext(null)
}
return {
state: newState,
}
},
moveRequest(
{ state }: RESTCollectionStoreType,
{
path,
requestIndex,
destinationPath,
}: { path: string; requestIndex: number; destinationPath: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve source path '${path}'. Skipping moveRequest dispatch.`
)
return {}
}
const req = targetLocation.requests[requestIndex]
const destIndexPaths = destinationPath.split("/").map((x) => parseInt(x))
const destLocation = navigateToFolderWithIndexPath(newState, destIndexPaths)
if (destLocation === null) {
console.log(
`Could not resolve destination path '${destinationPath}'. Skipping moveRequest dispatch.`
)
return {}
}
destLocation.requests.push(req)
targetLocation.requests.splice(requestIndex, 1)
return {
state: newState,
}
},
})
const gqlCollectionDispatchers = defineDispatchers({
setCollections(
_: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection<any>[] }
) {
return {
state: entries,
}
},
appendCollections(
{ state }: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection<any>[] }
) {
return {
state: [...state, ...entries],
}
},
addCollection(
{ state }: GraphqlCollectionStoreType,
{ collection }: { collection: HoppCollection<any> }
) {
return {
state: [...state, collection],
}
},
removeCollection(
{ state }: GraphqlCollectionStoreType,
{ collectionIndex }: { collectionIndex: number }
) {
return {
state: (state as any).filter(
(_: any, i: number) => i !== collectionIndex
),
}
},
editCollection(
{ state }: GraphqlCollectionStoreType,
{
collectionIndex,
collection,
}: { collectionIndex: number; collection: HoppCollection<any> }
) {
return {
state: state.map((col, index) =>
index === collectionIndex ? collection : col
),
}
},
addFolder(
{ state }: GraphqlCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection<HoppGQLRequest> = makeCollection({
name,
folders: [],
requests: [],
})
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(`Could not parse path '${path}'. Ignoring add folder request`)
return {}
}
target.folders.push(newFolder)
return {
state: newState,
}
},
editFolder(
{ state }: GraphqlCollectionStoreType,
{ path, folder }: { path: string; folder: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const target = navigateToFolderWithIndexPath(newState, indexPaths)
if (target === null) {
console.log(
`Could not parse path '${path}'. Ignoring edit folder request`
)
return {}
}
Object.assign(target, folder)
return {
state: newState,
}
},
removeFolder(
{ state }: GraphqlCollectionStoreType,
{ path }: { path: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
if (indexPaths.length === 0) {
console.log(
"Given path too short. If this is a collection, use removeCollection dispatcher instead. Skipping request."
)
return {}
}
// We get the index path to the folder itself,
// we have to find the folder containing the target folder,
// so we pop the last path index
const folderIndex = indexPaths.pop() as number
const containingFolder = navigateToFolderWithIndexPath(newState, indexPaths)
if (containingFolder === null) {
console.log(
`Could not resolve path '${path}'. Skipping removeFolder dispatch.`
)
return {}
}
containingFolder.folders.splice(folderIndex, 1)
return {
state: newState,
}
},
editRequest(
{ state }: GraphqlCollectionStoreType,
{
path,
requestIndex,
requestNew,
}: { path: string; requestIndex: number; requestNew: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring editRequest dispatch.`
)
return {}
}
targetLocation.requests = targetLocation.requests.map((req, index) =>
index !== requestIndex ? req : requestNew
)
return {
state: newState,
}
},
saveRequestAs(
{ state }: GraphqlCollectionStoreType,
{ path, request }: { path: string; request: any }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring saveRequestAs dispatch.`
)
return {}
}
targetLocation.requests.push(request)
return {
state: newState,
}
},
removeRequest(
{ state }: GraphqlCollectionStoreType,
{ path, requestIndex }: { path: string; requestIndex: number }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve path '${path}'. Ignoring removeRequest dispatch.`
)
return {}
}
targetLocation.requests.splice(requestIndex, 1)
// If the save context is set and is set to the same source, we invalidate it
const saveCtx = getRESTSaveContext()
if (
saveCtx?.originLocation === "user-collection" &&
saveCtx.folderPath === path &&
saveCtx.requestIndex === requestIndex
) {
setRESTSaveContext(null)
}
return {
state: newState,
}
},
moveRequest(
{ state }: GraphqlCollectionStoreType,
{
path,
requestIndex,
destinationPath,
}: { path: string; requestIndex: number; destinationPath: string }
) {
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
if (targetLocation === null) {
console.log(
`Could not resolve source path '${path}'. Skipping moveRequest dispatch.`
)
return {}
}
const req = targetLocation.requests[requestIndex]
const destIndexPaths = destinationPath.split("/").map((x) => parseInt(x))
const destLocation = navigateToFolderWithIndexPath(newState, destIndexPaths)
if (destLocation === null) {
console.log(
`Could not resolve destination path '${destinationPath}'. Skipping moveRequest dispatch.`
)
return {}
}
destLocation.requests.push(req)
targetLocation.requests.splice(requestIndex, 1)
return {
state: newState,
}
},
})
export const restCollectionStore = new DispatchingStore(
defaultRESTCollectionState,
restCollectionDispatchers
)
export const graphqlCollectionStore = new DispatchingStore(
defaultGraphqlCollectionState,
gqlCollectionDispatchers
)
export function setRESTCollections(entries: HoppCollection<HoppRESTRequest>[]) {
restCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
entries,
},
})
}
export const restCollections$ = restCollectionStore.subject$.pipe(
pluck("state")
)
export const graphqlCollections$ = graphqlCollectionStore.subject$.pipe(
pluck("state")
)
export function appendRESTCollections(
entries: HoppCollection<HoppRESTRequest>[]
) {
restCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
entries,
},
})
}
export function addRESTCollection(collection: HoppCollection<HoppRESTRequest>) {
restCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
collection,
},
})
}
export function removeRESTCollection(collectionIndex: number) {
restCollectionStore.dispatch({
dispatcher: "removeCollection",
payload: {
collectionIndex,
},
})
}
export function getRESTCollection(collectionIndex: number) {
return restCollectionStore.value.state[collectionIndex]
}
export function editRESTCollection(
collectionIndex: number,
collection: HoppCollection<HoppRESTRequest>
) {
restCollectionStore.dispatch({
dispatcher: "editCollection",
payload: {
collectionIndex,
collection,
},
})
}
export function addRESTFolder(name: string, path: string) {
restCollectionStore.dispatch({
dispatcher: "addFolder",
payload: {
name,
path,
},
})
}
export function editRESTFolder(
path: string,
folder: HoppCollection<HoppRESTRequest>
) {
restCollectionStore.dispatch({
dispatcher: "editFolder",
payload: {
path,
folder,
},
})
}
export function removeRESTFolder(path: string) {
restCollectionStore.dispatch({
dispatcher: "removeFolder",
payload: {
path,
},
})
}
export function editRESTRequest(
path: string,
requestIndex: number,
requestNew: HoppRESTRequest
) {
const indexPaths = path.split("/").map((x) => parseInt(x))
if (
!navigateToFolderWithIndexPath(restCollectionStore.value.state, indexPaths)
)
throw new Error("Path not found")
restCollectionStore.dispatch({
dispatcher: "editRequest",
payload: {
path,
requestIndex,
requestNew,
},
})
}
export function saveRESTRequestAs(path: string, request: HoppRESTRequest) {
// For calculating the insertion request index
const targetLocation = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((x) => parseInt(x))
)
const insertionIndex = targetLocation!.requests.length
restCollectionStore.dispatch({
dispatcher: "saveRequestAs",
payload: {
path,
request,
},
})
return insertionIndex
}
export function removeRESTRequest(path: string, requestIndex: number) {
restCollectionStore.dispatch({
dispatcher: "removeRequest",
payload: {
path,
requestIndex,
},
})
}
export function moveRESTRequest(
path: string,
requestIndex: number,
destinationPath: string
) {
restCollectionStore.dispatch({
dispatcher: "moveRequest",
payload: {
path,
requestIndex,
destinationPath,
},
})
}
export function setGraphqlCollections(
entries: HoppCollection<HoppGQLRequest>[]
) {
graphqlCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
entries,
},
})
}
export function appendGraphqlCollections(
entries: HoppCollection<HoppGQLRequest>[]
) {
graphqlCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
entries,
},
})
}
export function addGraphqlCollection(
collection: HoppCollection<HoppGQLRequest>
) {
graphqlCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
collection,
},
})
}
export function removeGraphqlCollection(collectionIndex: number) {
graphqlCollectionStore.dispatch({
dispatcher: "removeCollection",
payload: {
collectionIndex,
},
})
}
export function editGraphqlCollection(
collectionIndex: number,
collection: HoppCollection<HoppGQLRequest>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editCollection",
payload: {
collectionIndex,
collection,
},
})
}
export function addGraphqlFolder(name: string, path: string) {
graphqlCollectionStore.dispatch({
dispatcher: "addFolder",
payload: {
name,
path,
},
})
}
export function editGraphqlFolder(
path: string,
folder: HoppCollection<HoppGQLRequest>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editFolder",
payload: {
path,
folder,
},
})
}
export function removeGraphqlFolder(path: string) {
graphqlCollectionStore.dispatch({
dispatcher: "removeFolder",
payload: {
path,
},
})
}
export function editGraphqlRequest(
path: string,
requestIndex: number,
requestNew: HoppGQLRequest
) {
graphqlCollectionStore.dispatch({
dispatcher: "editRequest",
payload: {
path,
requestIndex,
requestNew,
},
})
}
export function saveGraphqlRequestAs(path: string, request: HoppGQLRequest) {
graphqlCollectionStore.dispatch({
dispatcher: "saveRequestAs",
payload: {
path,
request,
},
})
}
export function removeGraphqlRequest(path: string, requestIndex: number) {
graphqlCollectionStore.dispatch({
dispatcher: "removeRequest",
payload: {
path,
requestIndex,
},
})
}
export function moveGraphqlRequest(
path: string,
requestIndex: number,
destinationPath: string
) {
graphqlCollectionStore.dispatch({
dispatcher: "moveRequest",
payload: {
path,
requestIndex,
destinationPath,
},
})
}

View File

@@ -0,0 +1,593 @@
import { Environment } from "@hoppscotch/data"
import { cloneDeep, isEqual } from "lodash-es"
import { combineLatest, Observable } from "rxjs"
import { distinctUntilChanged, map, pluck } from "rxjs/operators"
import DispatchingStore, {
defineDispatchers,
} from "~/newstore/DispatchingStore"
type SelectedEnvironmentIndex =
| { type: "NO_ENV_SELECTED" }
| { type: "MY_ENV"; index: number }
| {
type: "TEAM_ENV"
teamID: string
teamEnvID: string
environment: Environment
}
const defaultEnvironmentsState = {
environments: [
{
name: "My Environment Variables",
variables: [],
},
] as Environment[],
globals: [] as Environment["variables"],
selectedEnvironmentIndex: {
type: "NO_ENV_SELECTED",
} as SelectedEnvironmentIndex,
}
type EnvironmentStore = typeof defaultEnvironmentsState
const dispatchers = defineDispatchers({
setSelectedEnvironmentIndex(
_: EnvironmentStore,
{
selectedEnvironmentIndex,
}: { selectedEnvironmentIndex: SelectedEnvironmentIndex }
) {
return {
selectedEnvironmentIndex,
}
},
appendEnvironments(
{ environments }: EnvironmentStore,
{ envs }: { envs: Environment[] }
) {
return {
environments: [...environments, ...envs],
}
},
replaceEnvironments(
_: EnvironmentStore,
{ environments }: { environments: Environment[] }
) {
return {
environments,
}
},
createEnvironment(
{ environments }: EnvironmentStore,
{ name }: { name: string }
) {
return {
environments: [
...environments,
{
name,
variables: [],
},
],
}
},
duplicateEnvironment(
{ environments }: EnvironmentStore,
{ envIndex }: { envIndex: number }
) {
const newEnvironment = environments.find((_, index) => index === envIndex)
if (!newEnvironment) {
return {
environments,
}
}
return {
environments: [
...environments,
{
...cloneDeep(newEnvironment),
name: `${newEnvironment.name} - Duplicate`,
},
],
}
},
deleteEnvironment(
{
environments,
// currentEnvironmentIndex,
selectedEnvironmentIndex,
}: EnvironmentStore,
{ envIndex }: { envIndex: number }
) {
let newCurrEnvIndex = selectedEnvironmentIndex
// Scenario 1: Currently Selected Env is removed -> Set currently selected to none
if (
selectedEnvironmentIndex.type === "MY_ENV" &&
envIndex === selectedEnvironmentIndex.index
)
newCurrEnvIndex = { type: "NO_ENV_SELECTED" }
// Scenario 2: Currently Selected Env Index > Deletion Index -> Current Selection Index Shifts One Position to the left -> Correct Env Index by moving back 1 index
if (
selectedEnvironmentIndex.type === "MY_ENV" &&
envIndex < selectedEnvironmentIndex.index
)
newCurrEnvIndex = {
type: "MY_ENV",
index: selectedEnvironmentIndex.index - 1,
}
// Scenario 3: Currently Selected Env Index < Deletion Index -> No change happens at selection position -> Noop
return {
environments: environments.filter((_, index) => index !== envIndex),
selectedEnvironmentIndex: newCurrEnvIndex,
}
},
renameEnvironment(
{ environments }: EnvironmentStore,
{ envIndex, newName }: { envIndex: number; newName: string }
) {
return {
environments: environments.map((env, index) =>
index === envIndex
? {
...env,
name: newName,
}
: env
),
}
},
updateEnvironment(
{ environments }: EnvironmentStore,
{ envIndex, updatedEnv }: { envIndex: number; updatedEnv: Environment }
) {
return {
environments: environments.map((env, index) =>
index === envIndex ? updatedEnv : env
),
}
},
addEnvironmentVariable(
{ environments }: EnvironmentStore,
{ envIndex, key, value }: { envIndex: number; key: string; value: string }
) {
return {
environments: environments.map((env, index) =>
index === envIndex
? {
...env,
variables: [...env.variables, { key, value }],
}
: env
),
}
},
removeEnvironmentVariable(
{ environments }: EnvironmentStore,
{ envIndex, variableIndex }: { envIndex: number; variableIndex: number }
) {
return {
environments: environments.map((env, index) =>
index === envIndex
? {
...env,
variables: env.variables.filter(
(_, vIndex) => vIndex !== variableIndex
),
}
: env
),
}
},
setEnvironmentVariables(
{ environments }: EnvironmentStore,
{
envIndex,
vars,
}: { envIndex: number; vars: { key: string; value: string }[] }
) {
return {
environments: environments.map((env, index) =>
index === envIndex
? {
...env,
variables: vars,
}
: env
),
}
},
updateEnvironmentVariable(
{ environments }: EnvironmentStore,
{
envIndex,
variableIndex,
updatedKey,
updatedValue,
}: {
envIndex: number
variableIndex: number
updatedKey: string
updatedValue: string
}
) {
return {
environments: environments.map((env, index) =>
index === envIndex
? {
...env,
variables: env.variables.map((v, vIndex) =>
vIndex === variableIndex
? { key: updatedKey, value: updatedValue }
: v
),
}
: env
),
}
},
setGlobalVariables(_, { entries }: { entries: Environment["variables"] }) {
return {
globals: entries,
}
},
clearGlobalVariables() {
return {
globals: [],
}
},
addGlobalVariable(
{ globals },
{ entry }: { entry: Environment["variables"][number] }
) {
return {
globals: [...globals, entry],
}
},
removeGlobalVariable({ globals }, { envIndex }: { envIndex: number }) {
return {
globals: globals.filter((_, i) => i !== envIndex),
}
},
updateGlobalVariable(
{ globals },
{
envIndex,
updatedEntry,
}: { envIndex: number; updatedEntry: Environment["variables"][number] }
) {
return {
globals: globals.map((x, i) => (i !== envIndex ? x : updatedEntry)),
}
},
})
export const environmentsStore = new DispatchingStore(
defaultEnvironmentsState,
dispatchers
)
export const environments$ = environmentsStore.subject$.pipe(
pluck("environments"),
distinctUntilChanged()
)
export const globalEnv$ = environmentsStore.subject$.pipe(
pluck("globals"),
distinctUntilChanged()
)
export const selectedEnvironmentIndex$ = environmentsStore.subject$.pipe(
pluck("selectedEnvironmentIndex"),
distinctUntilChanged()
)
export const currentEnvironment$ = environmentsStore.subject$.pipe(
map(({ environments, selectedEnvironmentIndex }) => {
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
const env: Environment = {
name: "No environment",
variables: [],
}
return env
} else if (selectedEnvironmentIndex.type === "MY_ENV") {
return environments[selectedEnvironmentIndex.index]
} else {
return selectedEnvironmentIndex.environment
}
})
)
export type AggregateEnvironment = {
key: string
value: string
sourceEnv: string
}
/**
* Stream returning all the environment variables accessible in
* the current state (Global + The Selected Environment).
* NOTE: The source environment attribute will be "Global" for Global Env as source.
*/
export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
[currentEnvironment$, globalEnv$]
).pipe(
map(([selectedEnv, globalVars]) => {
const results: AggregateEnvironment[] = []
selectedEnv.variables.forEach(({ key, value }) =>
results.push({ key, value, sourceEnv: selectedEnv.name })
)
globalVars.forEach(({ key, value }) =>
results.push({ key, value, sourceEnv: "Global" })
)
return results
}),
distinctUntilChanged(isEqual)
)
export function getAggregateEnvs() {
const currentEnv = getCurrentEnvironment()
return [
...currentEnv.variables.map(
(x) =>
<AggregateEnvironment>{
key: x.key,
value: x.value,
sourceEnv: currentEnv.name,
}
),
...getGlobalVariables().map(
(x) =>
<AggregateEnvironment>{
key: x.key,
value: x.value,
sourceEnv: "Global",
}
),
]
}
export function getCurrentEnvironment(): Environment {
if (
environmentsStore.value.selectedEnvironmentIndex.type === "NO_ENV_SELECTED"
) {
return {
name: "No environment",
variables: [],
}
} else if (
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
) {
return environmentsStore.value.environments[
environmentsStore.value.selectedEnvironmentIndex.index
]
} else {
return environmentsStore.value.selectedEnvironmentIndex.environment
}
}
export function getSelectedEnvironmentType() {
return environmentsStore.value.selectedEnvironmentIndex.type
}
export function setSelectedEnvironmentIndex(
selectedEnvironmentIndex: SelectedEnvironmentIndex
) {
environmentsStore.dispatch({
dispatcher: "setSelectedEnvironmentIndex",
payload: {
selectedEnvironmentIndex,
},
})
}
export function getLegacyGlobalEnvironment(): Environment | null {
const envs = environmentsStore.value.environments
const el = envs.find(
(env) => env.name === "globals" || env.name === "Globals"
)
return el ?? null
}
export function getGlobalVariables(): Environment["variables"] {
return environmentsStore.value.globals
}
export function addGlobalEnvVariable(entry: Environment["variables"][number]) {
environmentsStore.dispatch({
dispatcher: "addGlobalVariable",
payload: {
entry,
},
})
}
export function setGlobalEnvVariables(entries: Environment["variables"]) {
environmentsStore.dispatch({
dispatcher: "setGlobalVariables",
payload: {
entries,
},
})
}
export function clearGlobalEnvVariables() {
environmentsStore.dispatch({
dispatcher: "clearGlobalVariables",
payload: {},
})
}
export function removeGlobalEnvVariable(envIndex: number) {
environmentsStore.dispatch({
dispatcher: "removeGlobalVariable",
payload: {
envIndex,
},
})
}
export function updateGlobalEnvVariable(
envIndex: number,
updatedEntry: Environment["variables"][number]
) {
environmentsStore.dispatch({
dispatcher: "updateGlobalVariable",
payload: {
envIndex,
updatedEntry,
},
})
}
export function replaceEnvironments(newEnvironments: any[]) {
environmentsStore.dispatch({
dispatcher: "replaceEnvironments",
payload: {
environments: newEnvironments,
},
})
}
export function appendEnvironments(envs: Environment[]) {
environmentsStore.dispatch({
dispatcher: "appendEnvironments",
payload: {
envs,
},
})
}
export function createEnvironment(envName: string) {
environmentsStore.dispatch({
dispatcher: "createEnvironment",
payload: {
name: envName,
},
})
}
export function duplicateEnvironment(envIndex: number) {
environmentsStore.dispatch({
dispatcher: "duplicateEnvironment",
payload: {
envIndex,
},
})
}
export function deleteEnvironment(envIndex: number) {
environmentsStore.dispatch({
dispatcher: "deleteEnvironment",
payload: {
envIndex,
},
})
}
export function renameEnvironment(envIndex: number, newName: string) {
environmentsStore.dispatch({
dispatcher: "renameEnvironment",
payload: {
envIndex,
newName,
},
})
}
export function updateEnvironment(envIndex: number, updatedEnv: Environment) {
environmentsStore.dispatch({
dispatcher: "updateEnvironment",
payload: {
envIndex,
updatedEnv,
},
})
}
export function setEnvironmentVariables(
envIndex: number,
vars: { key: string; value: string }[]
) {
environmentsStore.dispatch({
dispatcher: "setEnvironmentVariables",
payload: {
envIndex,
vars,
},
})
}
export function addEnvironmentVariable(
envIndex: number,
{ key, value }: { key: string; value: string }
) {
environmentsStore.dispatch({
dispatcher: "addEnvironmentVariable",
payload: {
envIndex,
key,
value,
},
})
}
export function removeEnvironmentVariable(
envIndex: number,
variableIndex: number
) {
environmentsStore.dispatch({
dispatcher: "removeEnvironmentVariable",
payload: {
envIndex,
variableIndex,
},
})
}
export function updateEnvironmentVariable(
envIndex: number,
variableIndex: number,
{ key, value }: { key: string; value: string }
) {
environmentsStore.dispatch({
dispatcher: "updateEnvironmentVariable",
payload: {
envIndex,
variableIndex,
updatedKey: key,
updatedValue: value,
},
})
}
type SelectedEnv =
| { type: "NO_ENV_SELECTED" }
| { type: "MY_ENV"; index: number }
| { type: "TEAM_ENV" }
export function getEnvironment(selectedEnv: SelectedEnv) {
if (selectedEnv.type === "MY_ENV") {
return environmentsStore.value.environments[selectedEnv.index]
} else if (
selectedEnv.type === "TEAM_ENV" &&
environmentsStore.value.selectedEnvironmentIndex.type === "TEAM_ENV"
) {
return environmentsStore.value.selectedEnvironmentIndex.environment
} else {
return {
name: "N0_ENV",
variables: [],
}
}
}

View File

@@ -0,0 +1,331 @@
import { isEqual } from "lodash-es"
import { pluck } from "rxjs/operators"
import {
HoppRESTRequest,
translateToNewRequest,
HoppGQLRequest,
translateToGQLRequest,
GQL_REQ_SCHEMA_VERSION,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { completedRESTResponse$ } from "./RESTSession"
export type RESTHistoryEntry = {
v: number
request: HoppRESTRequest
responseMeta: {
duration: number | null
statusCode: number | null
}
star: boolean
id?: string // For when Firebase Firestore is set
updatedOn?: Date
}
export type GQLHistoryEntry = {
v: number
request: HoppGQLRequest
response: string
star: boolean
id?: string // For when Firestore ID is set
updatedOn?: Date
}
export function makeRESTHistoryEntry(
x: Omit<RESTHistoryEntry, "v">
): RESTHistoryEntry {
return {
v: 1,
...x,
}
}
export function makeGQLHistoryEntry(
x: Omit<GQLHistoryEntry, "v">
): GQLHistoryEntry {
return {
v: 1,
...x,
updatedOn: new Date(),
}
}
export function translateToNewRESTHistory(x: any): RESTHistoryEntry {
if (x.v === 1) return x
// Legacy
const request = translateToNewRequest(x)
const star = x.star ?? false
const duration = x.duration ?? null
const statusCode = x.status ?? null
const updatedOn = x.updatedOn ?? null
const obj: RESTHistoryEntry = makeRESTHistoryEntry({
request,
star,
responseMeta: {
duration,
statusCode,
},
updatedOn,
})
if (x.id) obj.id = x.id
return obj
}
export function translateToNewGQLHistory(x: any): GQLHistoryEntry {
if (x.v === 1 && x.request.v === GQL_REQ_SCHEMA_VERSION) return x
// Legacy
const request = x.request
? translateToGQLRequest(x.request)
: translateToGQLRequest(x)
const star = x.star ?? false
const response = x.response ?? ""
const updatedOn = x.updatedOn ?? ""
const obj: GQLHistoryEntry = makeGQLHistoryEntry({
request,
star,
response,
updatedOn,
})
if (x.id) obj.id = x.id
return obj
}
export const defaultRESTHistoryState = {
state: [] as RESTHistoryEntry[],
}
export const defaultGraphqlHistoryState = {
state: [] as GQLHistoryEntry[],
}
export const HISTORY_LIMIT = 50
type RESTHistoryType = typeof defaultRESTHistoryState
type GraphqlHistoryType = typeof defaultGraphqlHistoryState
const RESTHistoryDispatchers = defineDispatchers({
setEntries(_: RESTHistoryType, { entries }: { entries: RESTHistoryEntry[] }) {
return {
state: entries,
}
},
addEntry(
currentVal: RESTHistoryType,
{ entry }: { entry: RESTHistoryEntry }
) {
return {
state: [entry, ...currentVal.state].slice(0, HISTORY_LIMIT),
}
},
deleteEntry(
currentVal: RESTHistoryType,
{ entry }: { entry: RESTHistoryEntry }
) {
return {
state: currentVal.state.filter((e) => !isEqual(e, entry)),
}
},
clearHistory() {
return {
state: [],
}
},
toggleStar(
currentVal: RESTHistoryType,
{ entry }: { entry: RESTHistoryEntry }
) {
return {
state: currentVal.state.map((e) => {
if (isEqual(e, entry) && e.star !== undefined) {
return {
...e,
star: !e.star,
}
}
return e
}),
}
},
})
const GQLHistoryDispatchers = defineDispatchers({
setEntries(
_: GraphqlHistoryType,
{ entries }: { entries: GQLHistoryEntry[] }
) {
return {
state: entries,
}
},
addEntry(
currentVal: GraphqlHistoryType,
{ entry }: { entry: GQLHistoryEntry }
) {
return {
state: [entry, ...currentVal.state].slice(0, HISTORY_LIMIT),
}
},
deleteEntry(
currentVal: GraphqlHistoryType,
{ entry }: { entry: GQLHistoryEntry }
) {
return {
state: currentVal.state.filter((e) => !isEqual(e, entry)),
}
},
clearHistory() {
return {
state: [],
}
},
toggleStar(
currentVal: GraphqlHistoryType,
{ entry }: { entry: GQLHistoryEntry }
) {
return {
state: currentVal.state.map((e) => {
if (isEqual(e, entry) && e.star !== undefined) {
return {
...e,
star: !e.star,
}
}
return e
}),
}
},
})
export const restHistoryStore = new DispatchingStore(
defaultRESTHistoryState,
RESTHistoryDispatchers
)
export const graphqlHistoryStore = new DispatchingStore(
defaultGraphqlHistoryState,
GQLHistoryDispatchers
)
export const restHistory$ = restHistoryStore.subject$.pipe(pluck("state"))
export const graphqlHistory$ = graphqlHistoryStore.subject$.pipe(pluck("state"))
export function setRESTHistoryEntries(entries: RESTHistoryEntry[]) {
restHistoryStore.dispatch({
dispatcher: "setEntries",
payload: { entries },
})
}
export function addRESTHistoryEntry(entry: RESTHistoryEntry) {
restHistoryStore.dispatch({
dispatcher: "addEntry",
payload: { entry },
})
}
export function deleteRESTHistoryEntry(entry: RESTHistoryEntry) {
restHistoryStore.dispatch({
dispatcher: "deleteEntry",
payload: { entry },
})
}
export function clearRESTHistory() {
restHistoryStore.dispatch({
dispatcher: "clearHistory",
payload: {},
})
}
export function toggleRESTHistoryEntryStar(entry: RESTHistoryEntry) {
restHistoryStore.dispatch({
dispatcher: "toggleStar",
payload: { entry },
})
}
export function setGraphqlHistoryEntries(entries: GQLHistoryEntry[]) {
graphqlHistoryStore.dispatch({
dispatcher: "setEntries",
payload: { entries },
})
}
export function addGraphqlHistoryEntry(entry: GQLHistoryEntry) {
graphqlHistoryStore.dispatch({
dispatcher: "addEntry",
payload: { entry },
})
}
export function deleteGraphqlHistoryEntry(entry: GQLHistoryEntry) {
graphqlHistoryStore.dispatch({
dispatcher: "deleteEntry",
payload: { entry },
})
}
export function clearGraphqlHistory() {
graphqlHistoryStore.dispatch({
dispatcher: "clearHistory",
payload: {},
})
}
export function toggleGraphqlHistoryEntryStar(entry: GQLHistoryEntry) {
graphqlHistoryStore.dispatch({
dispatcher: "toggleStar",
payload: { entry },
})
}
// Listen to completed responses to add to history
completedRESTResponse$.subscribe((res) => {
if (res !== null) {
if (
res.type === "loading" ||
res.type === "network_fail" ||
res.type === "script_fail"
)
return
addRESTHistoryEntry(
makeRESTHistoryEntry({
request: {
auth: res.req.auth,
body: res.req.body,
endpoint: res.req.endpoint,
headers: res.req.headers,
method: res.req.method,
name: res.req.name,
params: res.req.params,
preRequestScript: res.req.preRequestScript,
testScript: res.req.testScript,
v: res.req.v,
},
responseMeta: {
duration: res.meta.responseDuration,
statusCode: res.statusCode,
},
star: false,
updatedOn: new Date(),
})
)
}
})

View File

@@ -0,0 +1,390 @@
/* eslint-disable no-restricted-globals, no-restricted-syntax */
import { clone, cloneDeep, assign, isEmpty } from "lodash-es"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/function"
import {
safelyExtractRESTRequest,
translateToNewRequest,
translateToNewRESTCollection,
translateToNewGQLCollection,
Environment,
} from "@hoppscotch/data"
import {
settingsStore,
bulkApplySettings,
defaultSettings,
applySetting,
HoppAccentColor,
HoppBgColor,
} from "./settings"
import {
restHistoryStore,
graphqlHistoryStore,
setRESTHistoryEntries,
setGraphqlHistoryEntries,
translateToNewRESTHistory,
translateToNewGQLHistory,
} from "./history"
import {
restCollectionStore,
graphqlCollectionStore,
setGraphqlCollections,
setRESTCollections,
} from "./collections"
import {
replaceEnvironments,
environments$,
addGlobalEnvVariable,
setGlobalEnvVariables,
globalEnv$,
setSelectedEnvironmentIndex,
selectedEnvironmentIndex$,
} from "./environments"
import {
getDefaultRESTRequest,
restRequest$,
setRESTRequest,
} from "./RESTSession"
import { WSRequest$, setWSRequest } from "./WebSocketSession"
import { SIORequest$, setSIORequest } from "./SocketIOSession"
import { SSERequest$, setSSERequest } from "./SSESession"
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
import { bulkApplyLocalState, localStateStore } from "./localstate"
import { StorageLike } from "@vueuse/core"
function checkAndMigrateOldSettings() {
const vuexData = JSON.parse(window.localStorage.getItem("vuex") || "{}")
if (isEmpty(vuexData)) return
const { postwoman } = vuexData
if (!isEmpty(postwoman?.settings)) {
const settingsData = assign(clone(defaultSettings), postwoman.settings)
window.localStorage.setItem("settings", JSON.stringify(settingsData))
delete postwoman.settings
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
}
if (postwoman?.collections) {
window.localStorage.setItem(
"collections",
JSON.stringify(postwoman.collections)
)
delete postwoman.collections
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
}
if (postwoman?.collectionsGraphql) {
window.localStorage.setItem(
"collectionsGraphql",
JSON.stringify(postwoman.collectionsGraphql)
)
delete postwoman.collectionsGraphql
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
}
if (postwoman?.environments) {
window.localStorage.setItem(
"environments",
JSON.stringify(postwoman.environments)
)
delete postwoman.environments
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
}
if (window.localStorage.getItem("THEME_COLOR")) {
const themeColor = window.localStorage.getItem("THEME_COLOR")
applySetting("THEME_COLOR", themeColor as HoppAccentColor)
window.localStorage.removeItem("THEME_COLOR")
}
if (window.localStorage.getItem("nuxt-color-mode")) {
const color = window.localStorage.getItem("nuxt-color-mode") as HoppBgColor
applySetting("BG_COLOR", color)
window.localStorage.removeItem("nuxt-color-mode")
}
}
function setupLocalStatePersistence() {
const localStateData = JSON.parse(
window.localStorage.getItem("localState") ?? "{}"
)
if (localStateData) bulkApplyLocalState(localStateData)
localStateStore.subject$.subscribe((state) => {
window.localStorage.setItem("localState", JSON.stringify(state))
})
}
function setupSettingsPersistence() {
const settingsData = JSON.parse(
window.localStorage.getItem("settings") || "{}"
)
if (settingsData) {
bulkApplySettings(settingsData)
}
settingsStore.subject$.subscribe((settings) => {
window.localStorage.setItem("settings", JSON.stringify(settings))
})
}
function setupHistoryPersistence() {
const restHistoryData = JSON.parse(
window.localStorage.getItem("history") || "[]"
).map(translateToNewRESTHistory)
const graphqlHistoryData = JSON.parse(
window.localStorage.getItem("graphqlHistory") || "[]"
).map(translateToNewGQLHistory)
setRESTHistoryEntries(restHistoryData)
setGraphqlHistoryEntries(graphqlHistoryData)
restHistoryStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem("history", JSON.stringify(state))
})
graphqlHistoryStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem("graphqlHistory", JSON.stringify(state))
})
}
function setupCollectionsPersistence() {
const restCollectionData = JSON.parse(
window.localStorage.getItem("collections") || "[]"
).map(translateToNewRESTCollection)
const graphqlCollectionData = JSON.parse(
window.localStorage.getItem("collectionsGraphql") || "[]"
).map(translateToNewGQLCollection)
setRESTCollections(restCollectionData)
setGraphqlCollections(graphqlCollectionData)
restCollectionStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem("collections", JSON.stringify(state))
})
graphqlCollectionStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem("collectionsGraphql", JSON.stringify(state))
})
}
function setupEnvironmentsPersistence() {
const environmentsData: Environment[] = JSON.parse(
window.localStorage.getItem("environments") || "[]"
)
// Check if a global env is defined and if so move that to globals
const globalIndex = environmentsData.findIndex(
(x) => x.name.toLowerCase() === "globals"
)
if (globalIndex !== -1) {
const globalEnv = environmentsData[globalIndex]
globalEnv.variables.forEach((variable) => addGlobalEnvVariable(variable))
// Remove global from environments
environmentsData.splice(globalIndex, 1)
// Just sync the changes manually
window.localStorage.setItem(
"environments",
JSON.stringify(environmentsData)
)
}
replaceEnvironments(environmentsData)
environments$.subscribe((envs) => {
window.localStorage.setItem("environments", JSON.stringify(envs))
})
}
function setupSelectedEnvPersistence() {
const selectedEnvIndex = pipe(
// Value from local storage can be nullable
O.fromNullable(window.localStorage.getItem("selectedEnvIndex")),
O.map(parseInt), // If not null, parse to integer
O.chain(
O.fromPredicate(
Number.isInteger // Check if the number is proper int (not NaN)
)
),
O.getOrElse(() => -1) // If all the above conditions pass, we are good, else set default value (-1)
)
// Check if current environment index is -1 ie. no environment is selected
if (selectedEnvIndex === -1) {
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else {
setSelectedEnvironmentIndex({
type: "MY_ENV",
index: selectedEnvIndex,
})
}
selectedEnvironmentIndex$.subscribe((envIndex) => {
if (envIndex.type === "MY_ENV") {
window.localStorage.setItem("selectedEnvIndex", envIndex.index.toString())
} else {
window.localStorage.setItem("selectedEnvIndex", "-1")
}
})
}
function setupWebsocketPersistence() {
const request = JSON.parse(
window.localStorage.getItem("WebsocketRequest") || "null"
)
setWSRequest(request)
WSRequest$.subscribe((req) => {
window.localStorage.setItem("WebsocketRequest", JSON.stringify(req))
})
}
function setupSocketIOPersistence() {
const request = JSON.parse(
window.localStorage.getItem("SocketIORequest") || "null"
)
setSIORequest(request)
SIORequest$.subscribe((req) => {
window.localStorage.setItem("SocketIORequest", JSON.stringify(req))
})
}
function setupSSEPersistence() {
const request = JSON.parse(
window.localStorage.getItem("SSERequest") || "null"
)
setSSERequest(request)
SSERequest$.subscribe((req) => {
window.localStorage.setItem("SSERequest", JSON.stringify(req))
})
}
function setupMQTTPersistence() {
const request = JSON.parse(
window.localStorage.getItem("MQTTRequest") || "null"
)
setMQTTRequest(request)
MQTTRequest$.subscribe((req) => {
window.localStorage.setItem("MQTTRequest", JSON.stringify(req))
})
}
function setupGlobalEnvsPersistence() {
const globals: Environment["variables"] = JSON.parse(
window.localStorage.getItem("globalEnv") || "[]"
)
setGlobalEnvVariables(globals)
globalEnv$.subscribe((vars) => {
window.localStorage.setItem("globalEnv", JSON.stringify(vars))
})
}
function setupRequestPersistence() {
const localRequest = JSON.parse(
window.localStorage.getItem("restRequest") || "null"
)
if (localRequest) {
const parsedLocal = translateToNewRequest(localRequest)
setRESTRequest(
safelyExtractRESTRequest(parsedLocal, getDefaultRESTRequest())
)
}
restRequest$.subscribe((req) => {
const reqClone = cloneDeep(req)
if (reqClone.body.contentType === "multipart/form-data") {
reqClone.body.body = reqClone.body.body.map((x) => {
if (x.isFile)
return {
...x,
isFile: false,
value: "",
}
else return x
})
}
window.localStorage.setItem("restRequest", JSON.stringify(reqClone))
})
}
export function setupLocalPersistence() {
checkAndMigrateOldSettings()
setupLocalStatePersistence()
setupSettingsPersistence()
setupRequestPersistence()
setupHistoryPersistence()
setupCollectionsPersistence()
setupGlobalEnvsPersistence()
setupEnvironmentsPersistence()
setupSelectedEnvPersistence()
setupWebsocketPersistence()
setupSocketIOPersistence()
setupSSEPersistence()
setupMQTTPersistence()
}
/**
* Gets a value in LocalStorage.
*
* NOTE: Use LocalStorage to only store non-reactive simple data
* For more complex data, use stores and connect it to localpersistence
*/
export function getLocalConfig(name: string) {
return window.localStorage.getItem(name)
}
/**
* Sets a value in LocalStorage.
*
* NOTE: Use LocalStorage to only store non-reactive simple data
* For more complex data, use stores and connect it to localpersistence
*/
export function setLocalConfig(key: string, value: string) {
window.localStorage.setItem(key, value)
}
/**
* Clear config value in LocalStorage.
* @param key Key to be cleared
*/
export function removeLocalConfig(key: string) {
window.localStorage.removeItem(key)
}
/**
* The storage system we are using in the application.
* NOTE: This is a placeholder for being used in app.
* This entire redirection of localStorage is to allow for
* not refactoring the entire app code when we refactor when
* we are building the native (which may lack localStorage,
* or use a custom system)
*/
export const hoppLocalConfigStorage: StorageLike = localStorage

View File

@@ -0,0 +1,67 @@
import { Ref } from "vue"
import { distinctUntilChanged, pluck } from "rxjs"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { useStream } from "@composables/stream"
type LocalState = {
REMEMBERED_TEAM_ID: string | undefined
}
const defaultLocalState: LocalState = {
REMEMBERED_TEAM_ID: undefined,
}
const dispatchers = defineDispatchers({
bulkApplyState(_currentState: LocalState, payload: Partial<LocalState>) {
return payload
},
applyState<K extends keyof LocalState>(
_currentState: LocalState,
{ key, value }: { key: K; value: LocalState[K] }
) {
const result: Partial<LocalState> = {
[key]: value,
}
return result
},
})
export const localStateStore = new DispatchingStore(
defaultLocalState,
dispatchers
)
export const localState$ = localStateStore.subject$.asObservable()
export function bulkApplyLocalState(obj: Partial<LocalState>) {
localStateStore.dispatch({
dispatcher: "bulkApplyState",
payload: obj,
})
}
export function applyLocalState<K extends keyof LocalState>(
key: K,
value: LocalState[K]
) {
localStateStore.dispatch({
dispatcher: "applyState",
payload: { key, value },
})
}
export function useLocalState<K extends keyof LocalState>(
key: K
): Ref<LocalState[K]> {
return useStream(
localStateStore.subject$.pipe(pluck(key), distinctUntilChanged()),
localStateStore.value[key],
(value: LocalState[K]) => {
localStateStore.dispatch({
dispatcher: "applyState",
payload: { key, value },
})
}
)
}

View File

@@ -0,0 +1,165 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import { has } from "lodash-es"
import { Observable } from "rxjs"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import type { KeysMatching } from "~/types/ts-utils"
export const HoppBgColors = ["system", "light", "dark", "black"] as const
export type HoppBgColor = typeof HoppBgColors[number]
export const HoppAccentColors = [
"green",
"teal",
"blue",
"indigo",
"purple",
"yellow",
"orange",
"red",
"pink",
] as const
export type HoppAccentColor = typeof HoppAccentColors[number]
export const HoppFontSizes = ["small", "medium", "large"] as const
export type HoppFontSize = typeof HoppFontSizes[number]
export type SettingsType = {
syncCollections: boolean
syncHistory: boolean
syncEnvironments: boolean
PROXY_ENABLED: boolean
PROXY_URL: string
EXTENSIONS_ENABLED: boolean
URL_EXCLUDES: {
auth: boolean
httpUser: boolean
httpPassword: boolean
bearerToken: boolean
oauth2Token: boolean
}
THEME_COLOR: HoppAccentColor
BG_COLOR: HoppBgColor
TELEMETRY_ENABLED: boolean
EXPAND_NAVIGATION: boolean
SIDEBAR: boolean
SIDEBAR_ON_LEFT: boolean
ZEN_MODE: boolean
FONT_SIZE: HoppFontSize
COLUMN_LAYOUT: boolean
}
export const defaultSettings: SettingsType = {
syncCollections: true,
syncHistory: true,
syncEnvironments: true,
PROXY_ENABLED: false,
PROXY_URL: "https://proxy.hoppscotch.io/",
EXTENSIONS_ENABLED: false,
URL_EXCLUDES: {
auth: true,
httpUser: true,
httpPassword: true,
bearerToken: true,
oauth2Token: true,
},
THEME_COLOR: "indigo",
BG_COLOR: "system",
TELEMETRY_ENABLED: true,
EXPAND_NAVIGATION: true,
SIDEBAR: true,
SIDEBAR_ON_LEFT: true,
ZEN_MODE: false,
FONT_SIZE: "small",
COLUMN_LAYOUT: true,
}
const validKeys = Object.keys(defaultSettings)
const dispatchers = defineDispatchers({
bulkApplySettings(
_currentState: SettingsType,
payload: Partial<SettingsType>
) {
return payload
},
toggleSetting(
currentState: SettingsType,
{ settingKey }: { settingKey: KeysMatching<SettingsType, boolean> }
) {
if (!has(currentState, settingKey)) {
// console.log(
// `Toggling of a non-existent setting key '${settingKey}' ignored`
// )
return {}
}
const result: Partial<SettingsType> = {}
result[settingKey] = !currentState[settingKey]
return result
},
applySetting<K extends keyof SettingsType>(
_currentState: SettingsType,
{ settingKey, value }: { settingKey: K; value: SettingsType[K] }
) {
if (!validKeys.includes(settingKey)) {
// console.log(
// `Ignoring non-existent setting key '${settingKey}' assignment`
// )
return {}
}
const result: Partial<SettingsType> = {}
result[settingKey] = value
return result
},
})
export const settingsStore = new DispatchingStore(defaultSettings, dispatchers)
/**
* An observable value to make avail all the state information at once
*/
export const settings$ = settingsStore.subject$.asObservable()
export function getSettingSubject<K extends keyof SettingsType>(
settingKey: K
): Observable<SettingsType[K]> {
return settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged())
}
export function bulkApplySettings(settingsObj: Partial<SettingsType>) {
settingsStore.dispatch({
dispatcher: "bulkApplySettings",
payload: settingsObj,
})
}
export function toggleSetting(settingKey: KeysMatching<SettingsType, boolean>) {
settingsStore.dispatch({
dispatcher: "toggleSetting",
payload: {
settingKey,
},
})
}
export function applySetting<K extends keyof SettingsType>(
settingKey: K,
value: SettingsType[K]
) {
settingsStore.dispatch({
dispatcher: "applySetting",
payload: {
settingKey,
value,
},
})
}