feat: UI of shortcode actions (#2347)
Co-authored-by: liyasthomas <liyascthomas@gmail.com> Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -152,37 +152,49 @@
|
||||
/>
|
||||
<div
|
||||
ref="saveTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
class="flex flex-col divide-y-1 divide-primaryDark focus:outline-none"
|
||||
tabindex="0"
|
||||
role="menu"
|
||||
@keyup.c="copyRequestAction.$el.click()"
|
||||
@keyup.s="saveRequestAction.$el.click()"
|
||||
@keyup.escape="saveOptions.tippy().hide()"
|
||||
>
|
||||
<SmartItem
|
||||
ref="copyRequestAction"
|
||||
:label="shareButtonText"
|
||||
:svg="copyLinkIcon"
|
||||
:loading="fetchingShareLink"
|
||||
:shortcut="['C']"
|
||||
@click.native="
|
||||
() => {
|
||||
copyRequest()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
ref="saveRequestAction"
|
||||
:label="`${t('request.save_as')}`"
|
||||
svg="folder-plus"
|
||||
:shortcut="['S']"
|
||||
@click.native="
|
||||
() => {
|
||||
showSaveRequestModal = true
|
||||
saveOptions.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<SmartItem
|
||||
ref="copyRequestAction"
|
||||
:label="shareButtonText"
|
||||
:svg="copyLinkIcon"
|
||||
:loading="fetchingShareLink"
|
||||
:shortcut="['C']"
|
||||
@click.native="
|
||||
() => {
|
||||
copyRequest()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartAnchor
|
||||
:label="`${t('request.view_my_links')}`"
|
||||
to="/profile"
|
||||
svg="arrow-right"
|
||||
reverse
|
||||
blank
|
||||
class="pb-3 -ml-4 text-tiny text-secondaryLight"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex pt-3 pb-1">
|
||||
<SmartItem
|
||||
ref="saveRequestAction"
|
||||
:label="`${t('request.save_as')}`"
|
||||
svg="folder-plus"
|
||||
:shortcut="['S']"
|
||||
@click.native="
|
||||
() => {
|
||||
showSaveRequestModal = true
|
||||
saveOptions.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</tippy>
|
||||
</span>
|
||||
|
||||
135
packages/hoppscotch-app/components/profile/Shortcode.vue
Normal file
135
packages/hoppscotch-app/components/profile/Shortcode.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div
|
||||
class="table-row-groups lg:flex block my-6 lg:my-0 w-full border lg:border-0 divide-y lg:divide-y-0 lg:divide-x divide-dividerLight border-dividerLight"
|
||||
>
|
||||
<div class="table-column lg:w-1/5" :data-label="t('shortcodes.short_code')">
|
||||
{{ shortcode.id }}
|
||||
</div>
|
||||
<div
|
||||
class="table-column lg:w-1/5"
|
||||
:class="requestLabelColor"
|
||||
:data-label="t('shortcodes.method')"
|
||||
>
|
||||
{{ parseShortcodeRequest.method }}
|
||||
</div>
|
||||
<div class="table-column lg:w-3/5" :data-label="t('shortcodes.url')">
|
||||
<div class="max-w-50 lg:max-w-90 truncate">
|
||||
{{ parseShortcodeRequest.endpoint }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="timeStampRef"
|
||||
class="table-column lg:w-1/5"
|
||||
:data-label="t('shortcodes.created_on')"
|
||||
>
|
||||
<span v-tippy="{ theme: 'tooltip' }" :title="timeStamp">
|
||||
{{ dateStamp }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-center lg:w-1/5 px-3"
|
||||
:data-label="t('shortcodes.actions')"
|
||||
>
|
||||
<SmartAnchor
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.open_workspace')"
|
||||
:to="`https://hopp.sh/r/${shortcode.id}`"
|
||||
blank
|
||||
svg="external-link"
|
||||
color="blue"
|
||||
class="px-3"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
color="green"
|
||||
:svg="copyIconRefs"
|
||||
class="px-3"
|
||||
@click.native="copyShortcode(shortcode.id)"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.delete')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
class="px-3"
|
||||
@click.native="deleteShortcode(shortcode.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "@nuxtjs/composition-api"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as RR from "fp-ts/ReadonlyRecord"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { translateToNewRequest } from "@hoppscotch/data"
|
||||
import { useI18n, useToast } from "~/helpers/utils/composables"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { Shortcode } from "~/helpers/shortcodes/Shortcode"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
shortcode: Shortcode
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete-shortcode", codeID: string): void
|
||||
}>()
|
||||
|
||||
const deleteShortcode = (codeID: string) => {
|
||||
emit("delete-shortcode", codeID)
|
||||
}
|
||||
|
||||
const requestMethodLabels = {
|
||||
get: "text-green-500",
|
||||
post: "text-yellow-500",
|
||||
put: "text-blue-500",
|
||||
delete: "text-red-500",
|
||||
default: "text-gray-500",
|
||||
} as const
|
||||
|
||||
const timeStampRef = ref()
|
||||
const copyIconRefs = ref<"copy" | "check">("copy")
|
||||
|
||||
const parseShortcodeRequest = computed(() =>
|
||||
pipe(props.shortcode.request, JSON.parse, translateToNewRequest)
|
||||
)
|
||||
|
||||
const requestLabelColor = computed(() =>
|
||||
pipe(
|
||||
requestMethodLabels,
|
||||
RR.lookup(parseShortcodeRequest.value.method.toLowerCase()),
|
||||
O.getOrElseW(() => requestMethodLabels.default)
|
||||
)
|
||||
)
|
||||
|
||||
const dateStamp = computed(() =>
|
||||
new Date(props.shortcode.createdOn).toLocaleDateString()
|
||||
)
|
||||
const timeStamp = computed(() =>
|
||||
new Date(props.shortcode.createdOn).toLocaleTimeString()
|
||||
)
|
||||
|
||||
const copyShortcode = (codeID: string) => {
|
||||
copyToClipboard(`https://hopp.sh/r/${codeID}`)
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
copyIconRefs.value = "check"
|
||||
setTimeout(() => (copyIconRefs.value = "copy"), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.table-column {
|
||||
@apply flex items-center justify-between px-3 py-3;
|
||||
}
|
||||
|
||||
.table-row-groups {
|
||||
.table-column {
|
||||
@apply before:text-secondary before:font-bold before:content-[attr(data-label)] lg:before:hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteShortcode($code: ID!) {
|
||||
revokeShortcode(code: $code)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query GetUserShortcodes($cursor: ID) {
|
||||
myShortcodes(cursor: $cursor) {
|
||||
id
|
||||
request
|
||||
createdOn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
subscription ShortcodeCreated {
|
||||
myShortcodesCreated {
|
||||
id
|
||||
request
|
||||
createdOn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
subscription ShortcodeDeleted {
|
||||
myShortcodesRevoked {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -4,8 +4,13 @@ import {
|
||||
CreateShortcodeDocument,
|
||||
CreateShortcodeMutation,
|
||||
CreateShortcodeMutationVariables,
|
||||
DeleteShortcodeDocument,
|
||||
DeleteShortcodeMutation,
|
||||
DeleteShortcodeMutationVariables,
|
||||
} from "../graphql"
|
||||
|
||||
type DeleteShortcodeErrors = "shortcode/not_found"
|
||||
|
||||
export const createShortcode = (request: HoppRESTRequest) =>
|
||||
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
|
||||
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,
|
||||
})
|
||||
|
||||
8
packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts
Normal file
8
packages/hoppscotch-app/helpers/shortcodes/Shortcode.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Defines how a Shortcode is represented in the ShortcodeListAdapter
|
||||
*/
|
||||
export interface Shortcode {
|
||||
id: string
|
||||
request: string
|
||||
createdOn: Date
|
||||
}
|
||||
@@ -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<GQLError<string> | null>
|
||||
loading$: BehaviorSubject<boolean>
|
||||
shortcodes$: BehaviorSubject<GetUserShortcodesQuery["myShortcodes"]>
|
||||
hasMoreShortcodes$: BehaviorSubject<boolean>
|
||||
|
||||
private timeoutHandle: ReturnType<typeof setTimeout> | null
|
||||
private isDispose: boolean
|
||||
|
||||
private myShortcodesCreated: Subscription | null
|
||||
private myShortcodesRevoked: Subscription | null
|
||||
|
||||
constructor(deferInit: boolean = false) {
|
||||
this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
|
||||
this.loading$ = new BehaviorSubject<boolean>(false)
|
||||
this.shortcodes$ = new BehaviorSubject<
|
||||
GetUserShortcodesQuery["myShortcodes"]
|
||||
>([])
|
||||
this.hasMoreShortcodes$ = new BehaviorSubject<boolean>(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
<div class="container">
|
||||
<div class="p-4">
|
||||
<div
|
||||
v-if="currentUser === null"
|
||||
v-if="loadingCurrentUser"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||
>
|
||||
<SmartSpinner class="mb-4" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="currentUser === null"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<img
|
||||
@@ -79,102 +85,195 @@
|
||||
</div>
|
||||
<SmartTabs v-model="selectedProfileTab">
|
||||
<SmartTab :id="'sync'" :label="t('settings.account')">
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.profile") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t("settings.profile_description") }}
|
||||
</div>
|
||||
<div class="py-4">
|
||||
<label for="displayName">
|
||||
{{ t("settings.profile_name") }}
|
||||
</label>
|
||||
<form
|
||||
class="flex mt-2 md:max-w-sm"
|
||||
@submit.prevent="updateDisplayName"
|
||||
>
|
||||
<input
|
||||
id="displayName"
|
||||
v-model="displayName"
|
||||
class="input"
|
||||
:placeholder="`${t('settings.profile_name')}`"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<ButtonSecondary
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="ml-2 min-w-16"
|
||||
type="submit"
|
||||
:loading="updatingDisplayName"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="py-4">
|
||||
<label for="emailAddress">
|
||||
{{ t("settings.profile_email") }}
|
||||
</label>
|
||||
<form
|
||||
class="flex mt-2 md:max-w-sm"
|
||||
@submit.prevent="updateEmailAddress"
|
||||
>
|
||||
<input
|
||||
id="emailAddress"
|
||||
v-model="emailAddress"
|
||||
class="input"
|
||||
:placeholder="`${t('settings.profile_name')}`"
|
||||
type="email"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<ButtonSecondary
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="ml-2 min-w-16"
|
||||
type="submit"
|
||||
:loading="updatingEmailAddress"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.sync") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t("settings.sync_description") }}
|
||||
</div>
|
||||
<div class="py-4 space-y-4">
|
||||
<div class="flex items-center">
|
||||
<SmartToggle
|
||||
:on="SYNC_COLLECTIONS"
|
||||
@change="toggleSetting('syncCollections')"
|
||||
>
|
||||
{{ t("settings.sync_collections") }}
|
||||
</SmartToggle>
|
||||
<div class="grid grid-cols-1">
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.profile") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t("settings.profile_description") }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<SmartToggle
|
||||
:on="SYNC_ENVIRONMENTS"
|
||||
@change="toggleSetting('syncEnvironments')"
|
||||
<div class="py-4">
|
||||
<label for="displayName">
|
||||
{{ t("settings.profile_name") }}
|
||||
</label>
|
||||
<form
|
||||
class="flex mt-2 md:max-w-sm"
|
||||
@submit.prevent="updateDisplayName"
|
||||
>
|
||||
{{ t("settings.sync_environments") }}
|
||||
</SmartToggle>
|
||||
<input
|
||||
id="displayName"
|
||||
v-model="displayName"
|
||||
class="input"
|
||||
:placeholder="`${t('settings.profile_name')}`"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<ButtonSecondary
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="ml-2 min-w-16"
|
||||
type="submit"
|
||||
:loading="updatingDisplayName"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<SmartToggle
|
||||
:on="SYNC_HISTORY"
|
||||
@change="toggleSetting('syncHistory')"
|
||||
<div class="py-4">
|
||||
<label for="emailAddress">
|
||||
{{ t("settings.profile_email") }}
|
||||
</label>
|
||||
<form
|
||||
class="flex mt-2 md:max-w-sm"
|
||||
@submit.prevent="updateEmailAddress"
|
||||
>
|
||||
{{ t("settings.sync_history") }}
|
||||
</SmartToggle>
|
||||
<input
|
||||
id="emailAddress"
|
||||
v-model="emailAddress"
|
||||
class="input"
|
||||
:placeholder="`${t('settings.profile_name')}`"
|
||||
type="email"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<ButtonSecondary
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="ml-2 min-w-16"
|
||||
type="submit"
|
||||
:loading="updatingEmailAddress"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.sync") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t("settings.sync_description") }}
|
||||
</div>
|
||||
<div class="py-4 space-y-4">
|
||||
<div class="flex items-center">
|
||||
<SmartToggle
|
||||
:on="SYNC_COLLECTIONS"
|
||||
@change="toggleSetting('syncCollections')"
|
||||
>
|
||||
{{ t("settings.sync_collections") }}
|
||||
</SmartToggle>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<SmartToggle
|
||||
:on="SYNC_ENVIRONMENTS"
|
||||
@change="toggleSetting('syncEnvironments')"
|
||||
>
|
||||
{{ t("settings.sync_environments") }}
|
||||
</SmartToggle>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<SmartToggle
|
||||
:on="SYNC_HISTORY"
|
||||
@change="toggleSetting('syncHistory')"
|
||||
>
|
||||
{{ t("settings.sync_history") }}
|
||||
</SmartToggle>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.short_codes") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t("settings.short_codes_description") }}
|
||||
</div>
|
||||
<div class="relative py-4 overflow-x-auto hide-scrollbar">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<SmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{
|
||||
t("state.loading")
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loading && myShortcodes.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_files.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-8"
|
||||
:alt="`${t('empty.shortcodes')}`"
|
||||
/>
|
||||
<span class="mb-4 text-center">
|
||||
{{ t("empty.shortcodes") }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!loading"
|
||||
class="table w-full border-collapse table-auto"
|
||||
>
|
||||
<div
|
||||
class="bg-primaryLight hidden lg:flex rounded-t w-full"
|
||||
>
|
||||
<div class="flex w-full">
|
||||
<div class="w-1/5 p-3 font-semibold">
|
||||
{{ t("shortcodes.short_code") }}
|
||||
</div>
|
||||
<div class="w-1/5 p-3 font-semibold">
|
||||
{{ t("shortcodes.method") }}
|
||||
</div>
|
||||
<div class="w-3/5 p-3 font-semibold">
|
||||
{{ t("shortcodes.url") }}
|
||||
</div>
|
||||
<div class="w-1/5 p-3 font-semibold">
|
||||
{{ t("shortcodes.created_on") }}
|
||||
</div>
|
||||
<div class="w-1/5 p-3 font-semibold text-center">
|
||||
{{ t("shortcodes.actions") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full max-h-sm flex flex-col items-center justify-between overflow-y-scroll rounded lg:rounded-t-none border lg:divide-y border-dividerLight divide-dividerLight"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col h-auto h-full border-r border-dividerLight w-full"
|
||||
>
|
||||
<ProfileShortcode
|
||||
v-for="(shortcode, shortcodeIndex) in myShortcodes"
|
||||
:key="`shortcode-${shortcodeIndex}`"
|
||||
:shortcode="shortcode"
|
||||
@delete-shortcode="deleteShortcode"
|
||||
/>
|
||||
<SmartIntersection
|
||||
v-if="hasMoreShortcodes && myShortcodes.length > 0"
|
||||
@intersecting="loadMoreShortcodes()"
|
||||
>
|
||||
<div
|
||||
v-if="adapterLoading"
|
||||
class="flex flex-col items-center py-3"
|
||||
>
|
||||
<SmartSpinner />
|
||||
</div>
|
||||
</SmartIntersection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<i class="mb-4 material-icons">help_outline</i>
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</SmartTab>
|
||||
<SmartTab :id="'teams'" :label="t('team.title')">
|
||||
<Teams :modal="false" />
|
||||
@@ -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<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("shortcodes.deleted")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
const loadMoreShortcodes = () => {
|
||||
adapter.loadMore()
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
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`,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user