feat: ability to delete user account and data (#2863)
* feat: add gql mutation * feat: added delete account section in profile page * feat: separate shortcodes section to a component * feat: delete user modal * feat: delete user account * feat: navigate to homepage after delete * chore: improve ui * fix: delete user mutation * chore: minor ui improvements * chore: correct grammar in certain i18n strings * feat: delection section separated to component * feat: separate user delete section into component * feat: defer fetch my teams * feat: disable delete account button on loading state * Update Shortcodes.vue Co-authored-by: Liyas Thomas <liyascthomas@gmail.com> Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -203,6 +203,9 @@
|
||||
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
||||
"check_console_details": "Check console log for details.",
|
||||
"curl_invalid_format": "cURL is not formatted properly",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Empty Request Name",
|
||||
"f12_details": "(F12 for details)",
|
||||
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
|
||||
@@ -437,6 +440,7 @@
|
||||
"settings": {
|
||||
"accent_color": "Accent color",
|
||||
"account": "Account",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_description": "Customize your account settings.",
|
||||
"account_email_description": "Your primary email address.",
|
||||
"account_name_description": "This is your display name.",
|
||||
@@ -445,6 +449,8 @@
|
||||
"change_font_size": "Change font size",
|
||||
"choose_language": "Choose language",
|
||||
"dark_mode": "Dark",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"expand_navigation": "Expand navigation",
|
||||
"experiments": "Experiments",
|
||||
"experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ",
|
||||
|
||||
@@ -98,20 +98,14 @@ declare module '@vue/runtime-core' {
|
||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
|
||||
@@ -123,6 +117,7 @@ declare module '@vue/runtime-core' {
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
|
||||
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
||||
ProfileShortcodes: typeof import('./components/profile/Shortcodes.vue')['default']
|
||||
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
|
||||
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
|
||||
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
||||
|
||||
170
packages/hoppscotch-common/src/components/profile/Shortcodes.vue
Normal file
170
packages/hoppscotch-common/src/components/profile/Shortcodes.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<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">
|
||||
<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">
|
||||
<div
|
||||
class="hidden w-full border-t rounded-t bg-primaryLight lg:flex border-x border-dividerLight"
|
||||
>
|
||||
<div class="flex w-full overflow-y-scroll">
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.short_code") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.method") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.url") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.created_on") }}
|
||||
</div>
|
||||
<div class="justify-center table-box">
|
||||
{{ t("shortcodes.actions") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center justify-between w-full overflow-y-scroll border rounded max-h-sm lg:rounded-t-none lg:divide-y border-dividerLight divide-dividerLight"
|
||||
>
|
||||
<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
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect, computed } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { currentUser$ } from "~/helpers/fb/auth"
|
||||
|
||||
import { onAuthEvent, onLoggedIn } from "@composables/auth"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { usePageHead } from "@composables/head"
|
||||
|
||||
import ShortcodeListAdapter from "~/helpers/shortcodes/ShortcodeListAdapter"
|
||||
import { deleteShortcode as backendDeleteShortcode } from "~/helpers/backend/mutations/Shortcode"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
usePageHead({
|
||||
title: computed(() => t("navigation.profile")),
|
||||
})
|
||||
|
||||
const currentUser = useReadonlyStream(currentUser$, null)
|
||||
|
||||
const displayName = ref(currentUser.value?.displayName)
|
||||
watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
||||
|
||||
const emailAddress = ref(currentUser.value?.email)
|
||||
watchEffect(() => (emailAddress.value = currentUser.value?.email))
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
onAuthEvent((ev) => {
|
||||
if (ev.event === "logout" && adapter.isInitialized()) {
|
||||
adapter.dispose()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-box {
|
||||
@apply flex flex-1 items-center px-4 py-2 truncate;
|
||||
}
|
||||
</style>
|
||||
187
packages/hoppscotch-common/src/components/profile/UserDelete.vue
Normal file
187
packages/hoppscotch-common/src/components/profile/UserDelete.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.delete_account") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight mb-4">
|
||||
{{ t("settings.delete_account_description") }}
|
||||
</div>
|
||||
|
||||
<ButtonSecondary
|
||||
filled
|
||||
outline
|
||||
:label="t('settings.delete_account')"
|
||||
type="submit"
|
||||
@click="showDeleteAccountModal = true"
|
||||
/>
|
||||
|
||||
<SmartModal
|
||||
v-if="showDeleteAccountModal"
|
||||
dialog
|
||||
:title="t('settings.delete_account')"
|
||||
@close="showDeleteAccountModal = false"
|
||||
>
|
||||
<template #body>
|
||||
<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-else-if="myTeams.length"
|
||||
class="flex flex-col p-4 space-y-2 border border-red-500 border-dashed rounded-lg text-secondaryDark bg-error"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
</h2>
|
||||
<div>
|
||||
{{ t("error.delete_account") }}
|
||||
<ul class="my-4 ml-8 space-y-2 list-disc">
|
||||
<li v-for="team in myTeams" :key="team.id">
|
||||
{{ team.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<span class="font-semibold">
|
||||
{{ t("error.delete_account_description") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
class="flex flex-col p-4 mb-4 space-y-2 border border-red-500 border-dashed rounded-lg text-secondaryDark bg-error"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
</h2>
|
||||
<div class="font-medium text-secondaryDark">
|
||||
{{ t("settings.delete_account_description") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="deleteUserAccount"
|
||||
v-model="userVerificationInput"
|
||||
class="input floating-input"
|
||||
placeholder=" "
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label for="deleteUserAccount">
|
||||
Type
|
||||
<span class="font-bold"> delete my account </span>
|
||||
to confirm
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
:label="t('settings.delete_account')"
|
||||
:loading="deletingUser"
|
||||
filled
|
||||
outline
|
||||
:disabled="
|
||||
loading ||
|
||||
myTeams.length > 0 ||
|
||||
userVerificationInput !== 'delete my account'
|
||||
"
|
||||
class="!bg-red-500 !hover:bg-red-600 !border-red-500 !hover:border-red-600"
|
||||
@click="deleteUserAccount"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="showDeleteAccountModal = false"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { ref, watch } from "vue"
|
||||
import { GQLError, runGQLQuery } from "~/helpers/backend/GQLClient"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { deleteUser } from "~/helpers/backend/mutations/Profile"
|
||||
import { signOutUser } from "~/helpers/fb/auth"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
const showDeleteAccountModal = ref(false)
|
||||
const userVerificationInput = ref("")
|
||||
|
||||
const loading = ref(true)
|
||||
const myTeams = ref<GetMyTeamsQuery["myTeams"]>([])
|
||||
|
||||
watch(showDeleteAccountModal, (isModalOpen) => {
|
||||
if (isModalOpen) {
|
||||
fetchMyTeams()
|
||||
}
|
||||
})
|
||||
|
||||
const fetchMyTeams = async () => {
|
||||
loading.value = true
|
||||
const result = await runGQLQuery({
|
||||
query: GetMyTeamsDocument,
|
||||
})
|
||||
loading.value = false
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
throw new Error(
|
||||
`Failed fetching teams list: ${JSON.stringify(result.left)}`
|
||||
)
|
||||
}
|
||||
|
||||
myTeams.value = result.right.myTeams.filter((team) => {
|
||||
return team.ownersCount === 1 && team.myRole === "OWNER"
|
||||
})
|
||||
}
|
||||
|
||||
const deletingUser = ref(false)
|
||||
|
||||
const deleteUserAccount = async () => {
|
||||
if (deletingUser.value) return
|
||||
deletingUser.value = true
|
||||
|
||||
pipe(
|
||||
deleteUser(),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
deletingUser.value = false
|
||||
toast.error(getErrorMessage(err))
|
||||
},
|
||||
() => {
|
||||
deletingUser.value = false
|
||||
showDeleteAccountModal.value = false
|
||||
toast.success(t("settings.account_deleted"))
|
||||
signOutUser()
|
||||
router.push(`/`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation DeleteUser {
|
||||
deleteUser
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { runMutation } from "../GQLClient"
|
||||
import {
|
||||
DeleteUserDocument,
|
||||
DeleteUserMutation,
|
||||
DeleteUserMutationVariables,
|
||||
} from "../graphql"
|
||||
|
||||
type DeleteUserErrors = "user/not_found"
|
||||
|
||||
export const deleteUser = () =>
|
||||
runMutation<
|
||||
DeleteUserMutation,
|
||||
DeleteUserMutationVariables,
|
||||
DeleteUserErrors
|
||||
>(DeleteUserDocument, {})
|
||||
@@ -64,7 +64,7 @@
|
||||
v-if="currentUser.emailVerified"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('settings.verified_email')"
|
||||
class="ml-2 text-green-500 svg-icons cursor-help"
|
||||
class="ml-2 text-green-500 svg-icons focus:outline-none cursor-help"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-else
|
||||
@@ -158,6 +158,9 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ProfileUserDelete />
|
||||
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.sync") }}
|
||||
@@ -192,90 +195,8 @@
|
||||
</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 flex-shrink-0 py-4 overflow-x-auto">
|
||||
<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">
|
||||
<div
|
||||
class="hidden w-full border-t rounded-t bg-primaryLight lg:flex border-x border-dividerLight"
|
||||
>
|
||||
<div class="flex w-full overflow-y-scroll">
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.short_code") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.method") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.url") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.created_on") }}
|
||||
</div>
|
||||
<div class="justify-center table-box">
|
||||
{{ t("shortcodes.actions") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center justify-between w-full overflow-y-scroll border rounded max-h-sm lg:rounded-t-none lg:divide-y border-dividerLight divide-dividerLight"
|
||||
>
|
||||
<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
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<component :is="IconHelpCircle" class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ProfileShortcodes />
|
||||
</div>
|
||||
</SmartTab>
|
||||
<SmartTab :id="'teams'" :label="t('team.title')">
|
||||
@@ -284,15 +205,13 @@
|
||||
</SmartTabs>
|
||||
</div>
|
||||
</div>
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect, computed } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
currentUser$,
|
||||
probableUser$,
|
||||
@@ -301,7 +220,6 @@ import {
|
||||
verifyEmailAddress,
|
||||
} from "~/helpers/fb/auth"
|
||||
|
||||
import { onAuthEvent, onLoggedIn } from "@composables/auth"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
@@ -310,13 +228,9 @@ import { useColorMode } from "@composables/theming"
|
||||
import { usePageHead } from "@composables/head"
|
||||
|
||||
import { toggleSetting } from "~/newstore/settings"
|
||||
import ShortcodeListAdapter from "~/helpers/shortcodes/ShortcodeListAdapter"
|
||||
import { deleteShortcode as backendDeleteShortcode } from "~/helpers/backend/mutations/Shortcode"
|
||||
|
||||
import IconVerified from "~icons/lucide/verified"
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
type ProfileTabs = "sync" | "teams"
|
||||
|
||||
@@ -393,62 +307,4 @@ const sendEmailVerification = () => {
|
||||
verifyingEmailAddress.value = false
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
onAuthEvent((ev) => {
|
||||
if (ev.event === "logout" && adapter.isInitialized()) {
|
||||
adapter.dispose()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-box {
|
||||
@apply flex flex-1 items-center px-4 py-2 truncate;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user