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] 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`, })