From df55807fa42c731be61560c9455328a94fa28aef Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Thu, 5 May 2022 20:07:13 +0530 Subject: [PATCH 01/50] fix: rest session not updating when the request is renamed from the sidebar (fixes #2297) --- .../components/collections/index.vue | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/hoppscotch-app/components/collections/index.vue b/packages/hoppscotch-app/components/collections/index.vue index 8034da5fe..c842ce96e 100644 --- a/packages/hoppscotch-app/components/collections/index.vue +++ b/packages/hoppscotch-app/components/collections/index.vue @@ -232,7 +232,11 @@ import { editRESTRequest, saveRESTRequestAs, } from "~/newstore/collections" -import { setRESTRequest, getRESTRequest } from "~/newstore/RESTSession" +import { + setRESTRequest, + getRESTRequest, + getRESTSaveContext, +} from "~/newstore/RESTSession" import { useReadonlyStream, useStreamSubscriber, @@ -495,12 +499,27 @@ export default defineComponent({ }, // Intented to by called by CollectionsEditRequest modal submit event updateEditingRequest(requestUpdateData) { + const saveCtx = getRESTSaveContext() + const requestUpdated = { ...this.editingRequest, name: requestUpdateData.name || this.editingRequest.name, } if (this.collectionsType.type === "my-collections") { + // Update REST Session with the updated state + if ( + saveCtx && + saveCtx.originLocation === "user-collection" && + saveCtx.requestIndex === this.editingRequestIndex && + saveCtx.folderPath === this.editingFolderPath + ) { + setRESTRequest({ + ...getRESTRequest(), + name: requestUpdateData.name, + }) + } + editRESTRequest( this.editingFolderPath, this.editingRequestIndex, @@ -515,6 +534,18 @@ export default defineComponent({ const requestName = requestUpdateData.name || this.editingRequest.name + // Update REST Session with the updated state + if ( + saveCtx && + saveCtx.originLocation === "team-collection" && + saveCtx.requestID === this.editingRequestIndex + ) { + setRESTRequest({ + ...getRESTRequest(), + name: requestUpdateData.name, + }) + } + runMutation(UpdateRequestDocument, { data: { request: JSON.stringify(requestUpdated), From c3b784c6805c89143fe5be6c8a94c2dea26cd9dc Mon Sep 17 00:00:00 2001 From: Nivedin <53208152+nivedin@users.noreply.github.com> Date: Thu, 5 May 2022 20:46:28 +0530 Subject: [PATCH 02/50] fix : save request popup bug (#2324) --- .../components/collections/SaveRequest.vue | 6 ++++++ .../hoppscotch-app/components/http/Request.vue | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/hoppscotch-app/components/collections/SaveRequest.vue b/packages/hoppscotch-app/components/collections/SaveRequest.vue index 06f775fc6..8f635dce5 100644 --- a/packages/hoppscotch-app/components/collections/SaveRequest.vue +++ b/packages/hoppscotch-app/components/collections/SaveRequest.vue @@ -233,6 +233,7 @@ const saveRequestAs = async () => { originLocation: "user-collection", folderPath: picked.value.folderPath, requestIndex: picked.value.requestIndex, + req: cloneDeep(requestUpdated), }) requestSaved() @@ -249,6 +250,7 @@ const saveRequestAs = async () => { originLocation: "user-collection", folderPath: picked.value.folderPath, requestIndex: insertionIndex, + req: cloneDeep(requestUpdated), }) requestSaved() @@ -265,6 +267,7 @@ const saveRequestAs = async () => { originLocation: "user-collection", folderPath: `${picked.value.collectionIndex}`, requestIndex: insertionIndex, + req: cloneDeep(requestUpdated), }) requestSaved() @@ -293,6 +296,7 @@ const saveRequestAs = async () => { setRESTSaveContext({ originLocation: "team-collection", requestID: picked.value.requestID, + req: cloneDeep(requestUpdated), }) } else if (picked.value.pickedType === "teams-folder") { if (!isHoppRESTRequest(requestUpdated)) @@ -319,6 +323,7 @@ const saveRequestAs = async () => { requestID: result.right.createRequestInCollection.id, teamID: collectionsType.value.selectedTeam.id, collectionID: picked.value.folderID, + req: cloneDeep(requestUpdated), }) requestSaved() @@ -348,6 +353,7 @@ const saveRequestAs = async () => { requestID: result.right.createRequestInCollection.id, teamID: collectionsType.value.selectedTeam.id, collectionID: picked.value.collectionID, + req: cloneDeep(requestUpdated), }) requestSaved() diff --git a/packages/hoppscotch-app/components/http/Request.vue b/packages/hoppscotch-app/components/http/Request.vue index 7939be844..4aa0594fb 100644 --- a/packages/hoppscotch-app/components/http/Request.vue +++ b/packages/hoppscotch-app/components/http/Request.vue @@ -208,6 +208,7 @@ import { computed, ref, watch } from "@nuxtjs/composition-api" import { isLeft, isRight } from "fp-ts/lib/Either" import * as E from "fp-ts/Either" +import cloneDeep from "lodash/cloneDeep" import { updateRESTResponse, restEndpoint$, @@ -477,14 +478,21 @@ const saveRequest = () => { showSaveRequestModal.value = true return } - if (saveCtx.originLocation === "user-collection") { + const req = getRESTRequest() + try { editRESTRequest( saveCtx.folderPath, saveCtx.requestIndex, getRESTRequest() ) + setRESTSaveContext({ + originLocation: "user-collection", + folderPath: saveCtx.folderPath, + requestIndex: saveCtx.requestIndex, + req: cloneDeep(req), + }) toast.success(`${t("request.saved")}`) } catch (e) { setRESTSaveContext(null) @@ -505,6 +513,11 @@ const saveRequest = () => { if (E.isLeft(result)) { toast.error(`${t("profile.no_permission")}`) } else { + setRESTSaveContext({ + originLocation: "team-collection", + requestID: saveCtx.requestID, + req: cloneDeep(req), + }) toast.success(`${t("request.saved")}`) } }) From 127bd7318f9417301762b41757a95f0ba2edcfd9 Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Sat, 7 May 2022 23:33:48 +0530 Subject: [PATCH 03/50] fix: improve indentation on GQL editors --- .../codemirror-lang-graphql/src/syntax.grammar | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/codemirror-lang-graphql/src/syntax.grammar b/packages/codemirror-lang-graphql/src/syntax.grammar index dd3f27346..6ba64976d 100644 --- a/packages/codemirror-lang-graphql/src/syntax.grammar +++ b/packages/codemirror-lang-graphql/src/syntax.grammar @@ -299,8 +299,8 @@ Description { } OperationType { - @specialize - | @specialize + @specialize + | @specialize | @specialize } @@ -317,7 +317,7 @@ ExecutableDirectiveLocation { @specialize | @specialize | @specialize - | @specialize + | @specialize | @specialize | @specialize | @specialize @@ -338,22 +338,24 @@ TypeSystemDirectiveLocation { | @specialize } -@skip { Whitespace | Comment } +@skip { whitespace | Comment } @tokens { - Whitespace { + whitespace { std.whitespace+ } + StringValue { "\"\"\"" (!["] | "\\n" | "\"" "\""? !["])* "\"\"\"" | "\"" !["\\\n]* "\"" } + IntValue { "-"? "0" | "-"? std.digit+ } FloatValue { - IntValue ("." std.digit+ | ("e" | "E") IntValue+) + IntValue ("." std.digit+ | ("e" | "E") IntValue+) } @precedence { IntValue, FloatValue } @@ -367,6 +369,8 @@ TypeSystemDirectiveLocation { Comma { "," } + + "{" "}" "[" "]" } -@detectDelim \ No newline at end of file +@detectDelim From fb1da491d8b22f6a1e07f99dea56c6454beeab8c Mon Sep 17 00:00:00 2001 From: Joel Jacob Stephen <70131076+JoelJacobStephen@users.noreply.github.com> Date: Tue, 10 May 2022 02:05:24 +0530 Subject: [PATCH 04/50] refactor: realtime log entry revamp (#2240) Co-authored-by: liyasthomas Co-authored-by: Andrew Bastin --- .../assets/icons/arrow-down-left.svg | 4 + .../assets/icons/arrow-down.svg | 4 + .../assets/icons/arrow-up-right.svg | 4 + .../hoppscotch-app/assets/icons/arrow-up.svg | 4 + .../assets/icons/chevrons-down.svg | 4 + .../assets/icons/chevrons-up.svg | 4 + .../assets/icons/info-disconnect.svg | 5 + .../assets/icons/info-realtime.svg | 5 + .../components/realtime/Log.vue | 145 ++++--- .../components/realtime/LogEntry.vue | 388 ++++++++++++++++++ .../components/realtime/Mqtt.vue | 51 ++- .../components/realtime/Socketio.vue | 36 +- .../components/realtime/Sse.vue | 42 +- .../components/realtime/Websocket.vue | 41 +- .../hoppscotch-app/helpers/functional/json.ts | 8 + .../hoppscotch-app/helpers/utils/string.ts | 12 - packages/hoppscotch-app/locales/en.json | 3 + pnpm-lock.yaml | 39 +- 18 files changed, 654 insertions(+), 145 deletions(-) create mode 100644 packages/hoppscotch-app/assets/icons/arrow-down-left.svg create mode 100644 packages/hoppscotch-app/assets/icons/arrow-down.svg create mode 100644 packages/hoppscotch-app/assets/icons/arrow-up-right.svg create mode 100644 packages/hoppscotch-app/assets/icons/arrow-up.svg create mode 100644 packages/hoppscotch-app/assets/icons/chevrons-down.svg create mode 100644 packages/hoppscotch-app/assets/icons/chevrons-up.svg create mode 100644 packages/hoppscotch-app/assets/icons/info-disconnect.svg create mode 100644 packages/hoppscotch-app/assets/icons/info-realtime.svg create mode 100644 packages/hoppscotch-app/components/realtime/LogEntry.vue delete mode 100644 packages/hoppscotch-app/helpers/utils/string.ts diff --git a/packages/hoppscotch-app/assets/icons/arrow-down-left.svg b/packages/hoppscotch-app/assets/icons/arrow-down-left.svg new file mode 100644 index 000000000..583988208 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-down-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/hoppscotch-app/assets/icons/arrow-down.svg b/packages/hoppscotch-app/assets/icons/arrow-down.svg new file mode 100644 index 000000000..ad365d5fc --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/arrow-up-right.svg b/packages/hoppscotch-app/assets/icons/arrow-up-right.svg new file mode 100644 index 000000000..6ac911836 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-up-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/hoppscotch-app/assets/icons/arrow-up.svg b/packages/hoppscotch-app/assets/icons/arrow-up.svg new file mode 100644 index 000000000..9966fa544 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/arrow-up.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/chevrons-down.svg b/packages/hoppscotch-app/assets/icons/chevrons-down.svg new file mode 100644 index 000000000..b46abe3a6 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/chevrons-down.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/chevrons-up.svg b/packages/hoppscotch-app/assets/icons/chevrons-up.svg new file mode 100644 index 000000000..d79fc4952 --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/chevrons-up.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/hoppscotch-app/assets/icons/info-disconnect.svg b/packages/hoppscotch-app/assets/icons/info-disconnect.svg new file mode 100644 index 000000000..ab55c86fd --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/info-disconnect.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/hoppscotch-app/assets/icons/info-realtime.svg b/packages/hoppscotch-app/assets/icons/info-realtime.svg new file mode 100644 index 000000000..ab55c86fd --- /dev/null +++ b/packages/hoppscotch-app/assets/icons/info-realtime.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/hoppscotch-app/components/realtime/Log.vue b/packages/hoppscotch-app/components/realtime/Log.vue index 348a2fab7..e46dc21d0 100644 --- a/packages/hoppscotch-app/components/realtime/Log.vue +++ b/packages/hoppscotch-app/components/realtime/Log.vue @@ -1,77 +1,128 @@ - + diff --git a/packages/hoppscotch-app/components/realtime/LogEntry.vue b/packages/hoppscotch-app/components/realtime/LogEntry.vue new file mode 100644 index 000000000..941455294 --- /dev/null +++ b/packages/hoppscotch-app/components/realtime/LogEntry.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/packages/hoppscotch-app/components/realtime/Mqtt.vue b/packages/hoppscotch-app/components/realtime/Mqtt.vue index 6cd482c20..b04000091 100644 --- a/packages/hoppscotch-app/components/realtime/Mqtt.vue +++ b/packages/hoppscotch-app/components/realtime/Mqtt.vue @@ -48,7 +48,11 @@ @@ -136,7 +140,8 @@ export default defineComponent({ { payload: this.$t("state.connecting_to", { name: this.server }), source: "info", - color: "var(--accent-color)", + event: "connecting", + ts: Date.now(), }, ] if (typeof EventSource !== "undefined") { @@ -149,8 +154,8 @@ export default defineComponent({ { payload: this.$t("state.connected_to", { name: this.server }), source: "info", - color: "var(--accent-color)", - ts: new Date().toLocaleTimeString(), + event: "connected", + ts: Date.now(), }, ] this.$toast.success(this.$t("state.connected")) @@ -164,9 +169,9 @@ export default defineComponent({ payload: this.$t("state.disconnected_from", { name: this.server, }), - source: "info", - color: "#ff5555", - ts: new Date().toLocaleTimeString(), + source: "disconnected", + event: "disconnected", + ts: Date.now(), }) this.$toast.error(this.$t("state.disconnected")) } @@ -174,7 +179,7 @@ export default defineComponent({ addSSELogLine({ payload: data, source: "server", - ts: new Date().toLocaleTimeString(), + ts: Date.now(), }) }) } catch (e) { @@ -185,9 +190,9 @@ export default defineComponent({ this.log = [ { payload: this.$t("error.browser_support_sse"), - source: "info", - color: "#ff5555", - ts: new Date().toLocaleTimeString(), + source: "disconnected", + event: "error", + ts: Date.now(), }, ] } @@ -201,22 +206,25 @@ export default defineComponent({ this.connectionSSEState = false addSSELogLine({ payload: this.$t("error.something_went_wrong"), - source: "info", - color: "#ff5555", - ts: new Date().toLocaleTimeString(), + source: "disconnected", + event: "error", + ts: Date.now(), }) if (error !== null) addSSELogLine({ payload: error, - source: "info", - color: "#ff5555", - ts: new Date().toLocaleTimeString(), + source: "disconnected", + event: "error", + ts: Date.now(), }) }, stop() { this.sse.close() this.sse.onclose() }, + clearLogEntries() { + this.log = [] + }, }, }) diff --git a/packages/hoppscotch-app/components/realtime/Websocket.vue b/packages/hoppscotch-app/components/realtime/Websocket.vue index cd1ce8ee5..9ac8705a8 100644 --- a/packages/hoppscotch-app/components/realtime/Websocket.vue +++ b/packages/hoppscotch-app/components/realtime/Websocket.vue @@ -137,22 +137,23 @@ class="inline-flex flex-col object-contain object-center w-16 h-16 my-4" :alt="$t('empty.protocols')" /> - - {{ $t("empty.protocols") }} - + {{ $t("empty.protocols") }} diff --git a/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts b/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts index 013155f04..254259f12 100644 --- a/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts +++ b/packages/hoppscotch-app/helpers/strategies/ExtensionStrategy.ts @@ -1,4 +1,5 @@ import * as TE from "fp-ts/TaskEither" +import * as O from "fp-ts/Option" import { pipe } from "fp-ts/function" import { AxiosRequestConfig } from "axios" import cloneDeep from "lodash/cloneDeep" @@ -15,12 +16,42 @@ export const hasFirefoxExtensionInstalled = () => hasExtensionInstalled() && browserIsFirefox() export const cancelRunningExtensionRequest = () => { - if ( - hasExtensionInstalled() && - window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest - ) { - window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest() + window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRunningRequest() +} + +export const defineSubscribableObject = (obj: T) => { + const proxyObject = { + ...obj, + _subscribers: {} as { + // eslint-disable-next-line no-unused-vars + [key in keyof T]?: ((...args: any[]) => any)[] + }, + subscribe(prop: keyof T, func: (...args: any[]) => any): void { + if (Array.isArray(this._subscribers[prop])) { + this._subscribers[prop]?.push(func) + } else { + this._subscribers[prop] = [func] + } + }, } + + type SubscribableProxyObject = typeof proxyObject + + return new Proxy(proxyObject, { + set(obj, prop, newVal) { + obj[prop as keyof SubscribableProxyObject] = newVal + + const currentSubscribers = obj._subscribers[prop as keyof T] + + if (Array.isArray(currentSubscribers)) { + for (const subscriber of currentSubscribers) { + subscriber(newVal) + } + } + + return true + }, + }) } const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => { @@ -56,13 +87,20 @@ const extensionStrategy: NetworkStrategy = (req) => // Run the request TE.bind("response", ({ processedReq }) => - TE.tryCatch( - () => - window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({ - ...processedReq, - wantsBinary: true, - }) as Promise, - (err) => err as any + pipe( + window.__POSTWOMAN_EXTENSION_HOOK__, + O.fromNullable, + TE.fromOption(() => "NO_PW_EXT_HOOK" as const), + TE.chain((extensionHook) => + TE.tryCatch( + () => + extensionHook.sendRequest({ + ...processedReq, + wantsBinary: true, + }), + (err) => err as any + ) + ) ) ), diff --git a/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js b/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js index b44a6f9af..53665d492 100644 --- a/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js +++ b/packages/hoppscotch-app/helpers/strategies/__tests__/ExtensionStrategy-NoProxy.spec.js @@ -122,13 +122,6 @@ describe("cancelRunningExtensionRequest", () => { cancelRunningExtensionRequest() expect(cancelFunc).not.toHaveBeenCalled() }) - - test("does not cancel request if extension installed but function not present", () => { - global.__POSTWOMAN_EXTENSION_HOOK__ = {} - - cancelRunningExtensionRequest() - expect(cancelFunc).not.toHaveBeenCalled() - }) }) describe("extensionStrategy", () => { diff --git a/packages/hoppscotch-app/layouts/default.vue b/packages/hoppscotch-app/layouts/default.vue index cf40259f3..971673e00 100644 --- a/packages/hoppscotch-app/layouts/default.vue +++ b/packages/hoppscotch-app/layouts/default.vue @@ -64,6 +64,8 @@ import { useRouter, watch, ref, + onMounted, + onBeforeUnmount, } from "@nuxtjs/composition-api" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" @@ -77,6 +79,12 @@ import { hookKeybindingsListener } from "~/helpers/keybindings" import { defineActionHandler } from "~/helpers/actions" import { useSentry } from "~/helpers/sentry" import { useColorMode } from "~/helpers/utils/composables" +import { + changeExtensionStatus, + ExtensionStatus, +} from "~/newstore/HoppExtension" + +import { defineSubscribableObject } from "~/helpers/strategies/ExtensionStrategy" function appLayout() { const rightSidebar = useSetting("SIDEBAR") @@ -202,6 +210,62 @@ function defineJumpActions() { }) } +function setupExtensionHooks() { + const extensionPollIntervalId = ref>() + + onMounted(() => { + if (window.__HOPP_EXTENSION_STATUS_PROXY__) { + changeExtensionStatus(window.__HOPP_EXTENSION_STATUS_PROXY__.status) + + window.__HOPP_EXTENSION_STATUS_PROXY__.subscribe( + "status", + (status: ExtensionStatus) => changeExtensionStatus(status) + ) + } else { + const statusProxy = defineSubscribableObject({ + status: "waiting" as ExtensionStatus, + }) + + window.__HOPP_EXTENSION_STATUS_PROXY__ = statusProxy + statusProxy.subscribe("status", (status: ExtensionStatus) => + changeExtensionStatus(status) + ) + + /** + * Keeping identifying extension backward compatible + * We are assuming the default version is 0.24 or later. So if the extension exists, its identified immediately, + * then we use a poll to find the version, this will get the version for 0.24 and any other version + * of the extension, but will have a slight lag. + * 0.24 users will get the benefits of 0.24, while the extension won't break for the old users + */ + extensionPollIntervalId.value = setInterval(() => { + if (typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined") { + if (extensionPollIntervalId.value) + clearInterval(extensionPollIntervalId.value) + + const version = window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() + + // When the version is not 0.24 or higher, the extension wont do this. so we have to do it manually + if ( + version.major === 0 && + version.minor <= 23 && + window.__HOPP_EXTENSION_STATUS_PROXY__ + ) { + window.__HOPP_EXTENSION_STATUS_PROXY__.status = "available" + } + } + }, 2000) + } + }) + + // Cleanup timer + onBeforeUnmount(() => { + if (extensionPollIntervalId.value) { + clearInterval(extensionPollIntervalId.value) + } + }) +} + export default defineComponent({ components: { Splitpanes, Pane }, setup() { @@ -229,6 +293,8 @@ export default defineComponent({ showSupport.value = !showSupport.value }) + setupExtensionHooks() + return { mdAndLarger, spacerClass, diff --git a/packages/hoppscotch-app/newstore/HoppExtension.ts b/packages/hoppscotch-app/newstore/HoppExtension.ts new file mode 100644 index 000000000..ae725333f --- /dev/null +++ b/packages/hoppscotch-app/newstore/HoppExtension.ts @@ -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 }, + }) +} diff --git a/packages/hoppscotch-app/pages/settings.vue b/packages/hoppscotch-app/pages/settings.vue index da460ad2b..78f9df3c3 100644 --- a/packages/hoppscotch-app/pages/settings.vue +++ b/packages/hoppscotch-app/pages/settings.vue @@ -241,14 +241,11 @@ import { useToast, useI18n, useColorMode, - usePolled, + useReadonlyStream, } from "~/helpers/utils/composables" -import { - hasExtensionInstalled, - hasChromeExtensionInstalled, - hasFirefoxExtensionInstalled, -} from "~/helpers/strategies/ExtensionStrategy" + import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent" +import { extensionStatus$ } from "~/newstore/HoppExtension" const t = useI18n() const toast = useToast() @@ -263,30 +260,21 @@ const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION") const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT") const ZEN_MODE = useSetting("ZEN_MODE") -const extensionVersion = usePolled(5000, (stopPolling) => { - const result = hasExtensionInstalled() - ? window.__POSTWOMAN_EXTENSION_HOOK__.getVersion() +const currentExtensionStatus = useReadonlyStream(extensionStatus$, null) + +const extensionVersion = computed(() => { + return currentExtensionStatus.value === "available" + ? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null : null - - // We don't need to poll anymore after we get value - if (result) stopPolling() - - return result }) -const hasChromeExtInstalled = usePolled(5000, (stopPolling) => { - // If not Chrome, we don't need to worry about this value changing - if (!browserIsChrome()) stopPolling() +const hasChromeExtInstalled = computed( + () => browserIsChrome() && currentExtensionStatus.value === "available" +) - return hasChromeExtensionInstalled() -}) - -const hasFirefoxExtInstalled = usePolled(5000, (stopPolling) => { - // If not Chrome, we don't need to worry about this value changing - if (!browserIsFirefox()) stopPolling() - - return hasFirefoxExtensionInstalled() -}) +const hasFirefoxExtInstalled = computed( + () => browserIsFirefox() && currentExtensionStatus.value === "available" +) const clearIcon = ref("rotate-ccw") diff --git a/packages/hoppscotch-app/types/pw-ext-hook.d.ts b/packages/hoppscotch-app/types/pw-ext-hook.d.ts index bdc172c38..8e39ff1e2 100644 --- a/packages/hoppscotch-app/types/pw-ext-hook.d.ts +++ b/packages/hoppscotch-app/types/pw-ext-hook.d.ts @@ -1,5 +1,6 @@ import { AxiosRequestConfig } from "axios" import { NetworkResponse } from "~/helpers/network" +import { ExtensionStatus } from "~/newstore/HoppExtension" export interface PWExtensionHook { getVersion: () => { major: number; minor: number } @@ -8,3 +9,11 @@ export interface PWExtensionHook { ) => Promise cancelRunningRequest: () => void } + +export type HoppExtensionStatusHook = { + status: ExtensionStatus + _subscribers: { + status?: ((...args: any[]) => any)[] | undefined + } + subscribe(prop: "status", func: (...args: any[]) => any): void +} diff --git a/packages/hoppscotch-app/types/window.d.ts b/packages/hoppscotch-app/types/window.d.ts index 81a6a9d02..e37081260 100644 --- a/packages/hoppscotch-app/types/window.d.ts +++ b/packages/hoppscotch-app/types/window.d.ts @@ -1,9 +1,8 @@ -import { PWExtensionHook } from "./pw-ext-hook" - -export {} +import { HoppExtensionStatusHook, PWExtensionHook } from "./pw-ext-hook" declare global { interface Window { - __POSTWOMAN_EXTENSION_HOOK__: PWExtensionHook + __POSTWOMAN_EXTENSION_HOOK__: PWExtensionHook | undefined + __HOPP_EXTENSION_STATUS_PROXY__: HoppExtensionStatusHook | undefined } } From cfa89a6dedd576089b3055d7c8133045c5e7a63c Mon Sep 17 00:00:00 2001 From: Nivedin <53208152+nivedin@users.noreply.github.com> Date: Tue, 24 May 2022 17:58:49 +0530 Subject: [PATCH 10/50] feat: UI of shortcode actions (#2347) Co-authored-by: liyasthomas Co-authored-by: Andrew Bastin --- .../components/http/Request.vue | 62 ++-- .../components/profile/Shortcode.vue | 135 +++++++ .../gql/mutations/DeleteShortcode.graphql | 3 + .../gql/queries/GetMyShortcodes.graphql | 7 + .../subscriptions/ShortcodeCreated.graphql | 7 + .../subscriptions/ShortcodeDeleted.graphql | 5 + .../hoppscotch-app/helpers/backend/helpers.ts | 2 +- .../helpers/backend/mutations/Shortcode.ts | 14 + .../helpers/shortcodes/Shortcode.ts | 8 + .../shortcodes/ShortcodeListAdapter.ts | 149 ++++++++ packages/hoppscotch-app/locales/en.json | 16 +- packages/hoppscotch-app/pages/profile.vue | 343 +++++++++++++----- 12 files changed, 632 insertions(+), 119 deletions(-) create mode 100644 packages/hoppscotch-app/components/profile/Shortcode.vue create mode 100644 packages/hoppscotch-app/helpers/backend/gql/mutations/DeleteShortcode.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/queries/GetMyShortcodes.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeCreated.graphql create mode 100644 packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeDeleted.graphql create mode 100644 packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts create mode 100644 packages/hoppscotch-app/helpers/shortcodes/ShortcodeListAdapter.ts diff --git a/packages/hoppscotch-app/components/http/Request.vue b/packages/hoppscotch-app/components/http/Request.vue index 4aa0594fb..fe280c994 100644 --- a/packages/hoppscotch-app/components/http/Request.vue +++ b/packages/hoppscotch-app/components/http/Request.vue @@ -152,37 +152,49 @@ /> diff --git a/packages/hoppscotch-app/components/profile/Shortcode.vue b/packages/hoppscotch-app/components/profile/Shortcode.vue new file mode 100644 index 000000000..7a48937f5 --- /dev/null +++ b/packages/hoppscotch-app/components/profile/Shortcode.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/packages/hoppscotch-app/helpers/backend/gql/mutations/DeleteShortcode.graphql b/packages/hoppscotch-app/helpers/backend/gql/mutations/DeleteShortcode.graphql new file mode 100644 index 000000000..38935eb18 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/mutations/DeleteShortcode.graphql @@ -0,0 +1,3 @@ +mutation DeleteShortcode($code: ID!) { + revokeShortcode(code: $code) +} \ No newline at end of file diff --git a/packages/hoppscotch-app/helpers/backend/gql/queries/GetMyShortcodes.graphql b/packages/hoppscotch-app/helpers/backend/gql/queries/GetMyShortcodes.graphql new file mode 100644 index 000000000..da986ca69 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/queries/GetMyShortcodes.graphql @@ -0,0 +1,7 @@ +query GetUserShortcodes($cursor: ID) { + myShortcodes(cursor: $cursor) { + id + request + createdOn + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeCreated.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeCreated.graphql new file mode 100644 index 000000000..557b90fa2 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeCreated.graphql @@ -0,0 +1,7 @@ +subscription ShortcodeCreated { + myShortcodesCreated { + id + request + createdOn + } +} diff --git a/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeDeleted.graphql b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeDeleted.graphql new file mode 100644 index 000000000..6c6506447 --- /dev/null +++ b/packages/hoppscotch-app/helpers/backend/gql/subscriptions/ShortcodeDeleted.graphql @@ -0,0 +1,5 @@ +subscription ShortcodeDeleted { + myShortcodesRevoked { + id + } +} diff --git a/packages/hoppscotch-app/helpers/backend/helpers.ts b/packages/hoppscotch-app/helpers/backend/helpers.ts index 9982374f9..3b1eac7bc 100644 --- a/packages/hoppscotch-app/helpers/backend/helpers.ts +++ b/packages/hoppscotch-app/helpers/backend/helpers.ts @@ -17,7 +17,7 @@ import { GetCollectionTitleDocument, } from "./graphql" -const BACKEND_PAGE_SIZE = 10 +export const BACKEND_PAGE_SIZE = 10 const getCollectionChildrenIDs = async (collID: string) => { const collsList: string[] = [] diff --git a/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts b/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts index 5ae84bc7c..e02759ab9 100644 --- a/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts +++ b/packages/hoppscotch-app/helpers/backend/mutations/Shortcode.ts @@ -4,8 +4,13 @@ import { CreateShortcodeDocument, CreateShortcodeMutation, CreateShortcodeMutationVariables, + DeleteShortcodeDocument, + DeleteShortcodeMutation, + DeleteShortcodeMutationVariables, } from "../graphql" +type DeleteShortcodeErrors = "shortcode/not_found" + export const createShortcode = (request: HoppRESTRequest) => runMutation( CreateShortcodeDocument, @@ -13,3 +18,12 @@ export const createShortcode = (request: HoppRESTRequest) => request: JSON.stringify(request), } ) + +export const deleteShortcode = (code: string) => + runMutation< + DeleteShortcodeMutation, + DeleteShortcodeMutationVariables, + DeleteShortcodeErrors + >(DeleteShortcodeDocument, { + code, + }) diff --git a/packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts b/packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts new file mode 100644 index 000000000..34039a719 --- /dev/null +++ b/packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts @@ -0,0 +1,8 @@ +/** + * Defines how a Shortcode is represented in the ShortcodeListAdapter + */ +export interface Shortcode { + id: string + request: string + createdOn: Date +} diff --git a/packages/hoppscotch-app/helpers/shortcodes/ShortcodeListAdapter.ts b/packages/hoppscotch-app/helpers/shortcodes/ShortcodeListAdapter.ts new file mode 100644 index 000000000..fbd15e30d --- /dev/null +++ b/packages/hoppscotch-app/helpers/shortcodes/ShortcodeListAdapter.ts @@ -0,0 +1,149 @@ +import * as E from "fp-ts/Either" +import { BehaviorSubject, Subscription } from "rxjs" +import { GQLError, runGQLQuery, runGQLSubscription } from "../backend/GQLClient" +import { + GetUserShortcodesQuery, + GetUserShortcodesDocument, + ShortcodeCreatedDocument, + ShortcodeDeletedDocument, +} from "../backend/graphql" +import { BACKEND_PAGE_SIZE } from "../backend/helpers" +import { Shortcode } from "./Shortcode" + +export default class ShortcodeListAdapter { + error$: BehaviorSubject | null> + loading$: BehaviorSubject + shortcodes$: BehaviorSubject + hasMoreShortcodes$: BehaviorSubject + + private timeoutHandle: ReturnType | null + private isDispose: boolean + + private myShortcodesCreated: Subscription | null + private myShortcodesRevoked: Subscription | null + + constructor(deferInit: boolean = false) { + this.error$ = new BehaviorSubject | null>(null) + this.loading$ = new BehaviorSubject(false) + this.shortcodes$ = new BehaviorSubject< + GetUserShortcodesQuery["myShortcodes"] + >([]) + this.hasMoreShortcodes$ = new BehaviorSubject(true) + this.timeoutHandle = null + this.isDispose = false + this.myShortcodesCreated = null + this.myShortcodesRevoked = null + + if (!deferInit) this.initialize() + } + + unsubscribeSubscriptions() { + this.myShortcodesCreated?.unsubscribe() + this.myShortcodesRevoked?.unsubscribe() + } + + initialize() { + if (this.timeoutHandle) throw new Error(`Adapter already initialized`) + if (this.isDispose) throw new Error(`Adapter has been disposed`) + + this.fetchList() + this.registerSubscriptions() + } + + public dispose() { + if (!this.timeoutHandle) throw new Error(`Adapter has not been initialized`) + if (!this.isDispose) throw new Error(`Adapter has been disposed`) + + this.isDispose = true + clearTimeout(this.timeoutHandle) + this.timeoutHandle = null + this.unsubscribeSubscriptions() + } + + fetchList() { + this.loadMore(true) + } + + async loadMore(forcedAttempt = false) { + if (!this.hasMoreShortcodes$.value && !forcedAttempt) return + + this.loading$.next(true) + + const lastCodeID = + this.shortcodes$.value.length > 0 + ? this.shortcodes$.value[this.shortcodes$.value.length - 1].id + : undefined + + const result = await runGQLQuery({ + query: GetUserShortcodesDocument, + variables: { + cursor: lastCodeID, + }, + }) + if (E.isLeft(result)) { + this.error$.next(result.left) + console.error(result.left) + this.loading$.next(false) + + throw new Error(`Failed fetching short codes list: ${result.left}`) + } + + const fetchedResult = result.right.myShortcodes + + this.pushNewShortcodes(fetchedResult) + + if (fetchedResult.length !== BACKEND_PAGE_SIZE) { + this.hasMoreShortcodes$.next(false) + } + + this.loading$.next(false) + } + + private pushNewShortcodes(results: Shortcode[]) { + const userShortcodes = this.shortcodes$.value + + userShortcodes.push(...results) + + this.shortcodes$.next(userShortcodes) + } + + private createShortcode(shortcode: Shortcode) { + const userShortcodes = this.shortcodes$.value + + userShortcodes.unshift(shortcode) + + this.shortcodes$.next(userShortcodes) + } + + private deleteShortcode(codeId: string) { + const newShortcodes = this.shortcodes$.value.filter( + ({ id }) => id !== codeId + ) + + this.shortcodes$.next(newShortcodes) + } + + private registerSubscriptions() { + this.myShortcodesCreated = runGQLSubscription({ + query: ShortcodeCreatedDocument, + }).subscribe((result) => { + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(`Shortcode Create Error ${result.left}`) + } + + this.createShortcode(result.right.myShortcodesCreated) + }) + + this.myShortcodesRevoked = runGQLSubscription({ + query: ShortcodeDeletedDocument, + }).subscribe((result) => { + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(`Shortcode Delete Error ${result.left}`) + } + + this.deleteShortcode(result.right.myShortcodesRevoked.id) + }) + } +} diff --git a/packages/hoppscotch-app/locales/en.json b/packages/hoppscotch-app/locales/en.json index 77b0759c1..458ac5f19 100644 --- a/packages/hoppscotch-app/locales/en.json +++ b/packages/hoppscotch-app/locales/en.json @@ -21,6 +21,7 @@ "more": "More", "new": "New", "no": "No", + "open_workspace": "Open workspace", "paste": "Paste", "prettify": "Prettify", "remove": "Remove", @@ -167,6 +168,7 @@ "profile": "Login in to view your profile", "protocols": "Protocols are empty", "schema": "Connect to a GraphQL endpoint to view schema", + "shortcodes": "Shortcodes are empty", "team_name": "Team name empty", "teams": "You don't belong to any teams", "tests": "There are no tests for this request" @@ -367,7 +369,8 @@ "title": "Request", "type": "Request type", "url": "URL", - "variables": "Variables" + "variables": "Variables", + "view_my_links": "View my links" }, "response": { "body": "Response Body", @@ -422,6 +425,8 @@ "proxy_use_toggle": "Use the proxy middleware to send requests", "read_the": "Read the", "reset_default": "Reset to default", + "short_codes": "Short codes", + "short_codes_description": "Short codes which were created by you.", "sidebar_on_left": "Sidebar on left", "sync": "Synchronise", "sync_collections": "Collections", @@ -483,6 +488,15 @@ "title": "Theme" } }, + "shortcodes":{ + "actions":"Actions", + "created_on": "Created on", + "deleted" : "Shortcode deleted", + "method": "Method", + "not_found":"Shortcode not found", + "short_code":"Short code", + "url": "URL" + }, "show": { "code": "Show code", "collection": "Expand Collection Panel", diff --git a/packages/hoppscotch-app/pages/profile.vue b/packages/hoppscotch-app/pages/profile.vue index 9d5f64f85..c0a782c72 100644 --- a/packages/hoppscotch-app/pages/profile.vue +++ b/packages/hoppscotch-app/pages/profile.vue @@ -3,7 +3,13 @@
+ +
+
-
-

- {{ t("settings.profile") }} -

-
- {{ t("settings.profile_description") }} -
-
- -
- - - -
-
- -
- - - -
-
-
-

- {{ t("settings.sync") }} -

-
- {{ t("settings.sync_description") }} -
-
-
- - {{ t("settings.sync_collections") }} - +
+
+

+ {{ t("settings.profile") }} +

+
+ {{ t("settings.profile_description") }}
-
- + +
- {{ t("settings.sync_environments") }} - + + +
-
- + +
- {{ t("settings.sync_history") }} - + + +
-
-
+ +
+

+ {{ t("settings.sync") }} +

+
+ {{ t("settings.sync_description") }} +
+
+
+ + {{ t("settings.sync_collections") }} + +
+
+ + {{ t("settings.sync_environments") }} + +
+
+ + {{ t("settings.sync_history") }} + +
+
+
+
+

+ {{ t("settings.short_codes") }} +

+
+ {{ t("settings.short_codes_description") }} +
+
+
+ + {{ + t("state.loading") + }} +
+
+ + + {{ t("empty.shortcodes") }} + +
+
+ +
+
+ + +
+ +
+
+
+
+
+
+ help_outline + {{ getErrorMessage(adapterError) }} +
+
+
+
@@ -193,12 +292,18 @@ import { useMeta, defineComponent, watchEffect, + computed, } from "@nuxtjs/composition-api" +import { pipe } from "fp-ts/function" +import * as TE from "fp-ts/TaskEither" +import { GQLError } from "~/helpers/backend/GQLClient" import { currentUser$, + probableUser$, setDisplayName, setEmailAddress, verifyEmailAddress, + onLoggedIn, } from "~/helpers/fb/auth" import { useReadonlyStream, @@ -206,6 +311,8 @@ import { useToast, } from "~/helpers/utils/composables" import { toggleSetting, useSetting } from "~/newstore/settings" +import ShortcodeListAdapter from "~/helpers/shortcodes/ShortcodeListAdapter" +import { deleteShortcode as backendDeleteShortcode } from "~/helpers/backend/mutations/Shortcode" type ProfileTabs = "sync" | "teams" @@ -220,6 +327,13 @@ const SYNC_COLLECTIONS = useSetting("syncCollections") const SYNC_ENVIRONMENTS = useSetting("syncEnvironments") const SYNC_HISTORY = useSetting("syncHistory") const currentUser = useReadonlyStream(currentUser$, null) +const probableUser = useReadonlyStream(probableUser$, null) + +const loadingCurrentUser = computed(() => { + if (!probableUser.value) return false + else if (!currentUser.value) return true + else return false +}) const displayName = ref(currentUser.value?.displayName) const updatingDisplayName = ref(false) @@ -273,6 +387,51 @@ const sendEmailVerification = () => { }) } +const adapter = new ShortcodeListAdapter(true) +const adapterLoading = useReadonlyStream(adapter.loading$, false) +const adapterError = useReadonlyStream(adapter.error$, null) +const myShortcodes = useReadonlyStream(adapter.shortcodes$, []) +const hasMoreShortcodes = useReadonlyStream(adapter.hasMoreShortcodes$, true) + +const loading = computed( + () => adapterLoading.value && myShortcodes.value.length === 0 +) + +onLoggedIn(() => { + adapter.initialize() +}) + +const deleteShortcode = (codeID: string) => { + pipe( + backendDeleteShortcode(codeID), + TE.match( + (err: GQLError) => { + toast.error(`${getErrorMessage(err)}`) + }, + () => { + toast.success(`${t("shortcodes.deleted")}`) + } + ) + )() +} + +const loadMoreShortcodes = () => { + adapter.loadMore() +} + +const getErrorMessage = (err: GQLError) => { + if (err.type === "network_error") { + return t("error.network_error") + } else { + switch (err.error) { + case "shortcode/not_found": + return t("shortcodes.not_found") + default: + return t("error.something_went_wrong") + } + } +} + useMeta({ title: `${t("navigation.profile")} • Hoppscotch`, }) From 346ac8bde91e29cf7be8968ef157525cfae59377 Mon Sep 17 00:00:00 2001 From: liyasthomas Date: Tue, 24 May 2022 18:48:45 +0530 Subject: [PATCH 11/50] chore: improve ui consistency on table layout --- .../components/profile/Shortcode.vue | 18 ++++++++++-------- packages/hoppscotch-app/pages/profile.vue | 16 ++++++++++------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/hoppscotch-app/components/profile/Shortcode.vue b/packages/hoppscotch-app/components/profile/Shortcode.vue index 7a48937f5..045789d43 100644 --- a/packages/hoppscotch-app/components/profile/Shortcode.vue +++ b/packages/hoppscotch-app/components/profile/Shortcode.vue @@ -2,24 +2,27 @@
-
+
{{ shortcode.id }}
{{ parseShortcodeRequest.method }}
-
+
{{ parseShortcodeRequest.endpoint }}
@@ -27,7 +30,7 @@
{