feat(common): add better ux to profile page by enabling routing for each tab (#5544)
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
parent
a5fb7cb0d2
commit
16f08e2a50
13 changed files with 408 additions and 199 deletions
|
|
@ -1037,6 +1037,8 @@
|
|||
"no_collection": "No collection",
|
||||
"collection_deleted": "associated collection deleted.",
|
||||
"private_access_hint": "For private mock servers, include the header 'x-api-key' with your Personal Access Token (create one from your profile).",
|
||||
"private_access_instruction": "To access this private mock server, include the header 'x-api-key' with your Personal Access Token.",
|
||||
"create_token_here": "Create here",
|
||||
"status": "Status",
|
||||
"server_running": "Server is running",
|
||||
"server_stopped": "Server is stopped",
|
||||
|
|
|
|||
|
|
@ -327,7 +327,6 @@ declare module 'vue' {
|
|||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
|
||||
TabsNav: typeof import('./components/TabsNav.vue')['default']
|
||||
Teams: typeof import('./components/teams/index.vue')['default']
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
||||
TeamsEdit: typeof import('./components/teams/Edit.vue')['default']
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
|
||||
|
|
|
|||
129
packages/hoppscotch-common/src/components/TabsNav.vue
Normal file
129
packages/hoppscotch-common/src/components/TabsNav.vue
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<div
|
||||
class="tabs relative border-dividerLight"
|
||||
:class="[vertical ? 'border-r' : 'border-b', styles]"
|
||||
>
|
||||
<div class="flex flex-1">
|
||||
<div class="flex flex-1">
|
||||
<router-link
|
||||
v-for="(item, index) in items"
|
||||
:key="`nav-${index}`"
|
||||
:to="item.route"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
placement: 'left',
|
||||
content: vertical ? item.label : null,
|
||||
}"
|
||||
active-class="active"
|
||||
exact-active-class="active"
|
||||
:exact="item.exactMatch"
|
||||
class="tab"
|
||||
:class="[{ vertical: vertical }]"
|
||||
:aria-label="item.label || ''"
|
||||
role="link"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
v-if="item.icon"
|
||||
class="svg-icons"
|
||||
:class="{ 'mr-2': item.label && !vertical }"
|
||||
/>
|
||||
<span v-if="item.label && !vertical">{{ item.label }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue"
|
||||
|
||||
export type NavItem = {
|
||||
label: string | null
|
||||
icon: string | Component | null
|
||||
route: string
|
||||
exactMatch?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
items: readonly NavItem[]
|
||||
styles?: string
|
||||
vertical?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabs {
|
||||
@apply flex;
|
||||
@apply whitespace-nowrap;
|
||||
@apply overflow-auto;
|
||||
@apply flex-shrink-0;
|
||||
|
||||
.tab {
|
||||
@apply relative;
|
||||
@apply flex;
|
||||
@apply flex-shrink-0;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply px-4 py-2;
|
||||
@apply text-secondary;
|
||||
@apply font-semibold;
|
||||
@apply cursor-pointer;
|
||||
@apply hover:text-secondaryDark;
|
||||
@apply focus:outline-none;
|
||||
@apply focus-visible:text-secondaryDark;
|
||||
@apply after:absolute;
|
||||
@apply after:left-4;
|
||||
@apply after:right-4;
|
||||
@apply after:bottom-0;
|
||||
@apply after:bg-transparent;
|
||||
@apply after:z-[2];
|
||||
@apply after:h-0.5;
|
||||
@apply after:content-[''];
|
||||
@apply focus:after:bg-divider;
|
||||
|
||||
.tab-info {
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply px-1;
|
||||
@apply min-w-[1rem];
|
||||
@apply h-4;
|
||||
@apply ml-2;
|
||||
@apply text-[8px];
|
||||
@apply border border-divider;
|
||||
@apply rounded;
|
||||
@apply text-secondaryLight;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply text-secondaryDark;
|
||||
@apply after:bg-accent;
|
||||
|
||||
.tab-info {
|
||||
@apply text-secondary;
|
||||
@apply border-dividerDark;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
@apply p-2;
|
||||
@apply rounded;
|
||||
@apply focus:after:hidden;
|
||||
|
||||
&.active {
|
||||
@apply text-accent;
|
||||
@apply after:hidden;
|
||||
|
||||
.tab-info {
|
||||
@apply text-secondary;
|
||||
@apply border-dividerDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -120,7 +120,15 @@
|
|||
</div>
|
||||
<!-- Hint for private mock servers -->
|
||||
<div v-if="!isPublic" class="w-full mt-2 text-xs text-secondaryLight">
|
||||
{{ t("mock_server.private_access_hint") }}
|
||||
{{ t("mock_server.private_access_instruction") }}
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
to="/profile/tokens"
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
:label="t('mock_server.create_token_here')"
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Set in Environment Toggle -->
|
||||
|
|
@ -202,6 +210,7 @@ import IconCopy from "~icons/lucide/copy"
|
|||
import IconPlay from "~icons/lucide/play"
|
||||
import IconServer from "~icons/lucide/server"
|
||||
import IconSquare from "~icons/lucide/square"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
|
|
|||
|
|
@ -157,7 +157,15 @@
|
|||
</div>
|
||||
<!-- Hint for private mock servers -->
|
||||
<div v-if="!isPublic" class="w-full mt-2 text-xs text-secondaryLight">
|
||||
{{ t("mock_server.private_access_hint") }}
|
||||
{{ t("mock_server.private_access_instruction") }}
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
to="/profile/tokens"
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
:label="t('mock_server.create_token_here')"
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Set in Environment Toggle -->
|
||||
|
|
@ -309,6 +317,7 @@ import IconCheck from "~icons/lucide/check"
|
|||
import IconPlay from "~icons/lucide/play"
|
||||
import IconServer from "~icons/lucide/server"
|
||||
import IconSquare from "~icons/lucide/square"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
|
|
|||
|
|
@ -118,7 +118,15 @@
|
|||
</span>
|
||||
</div>
|
||||
<div v-if="!isPublic" class="text-xs text-secondaryLight">
|
||||
{{ t("mock_server.private_access_hint") }}
|
||||
{{ t("mock_server.private_access_instruction") }}
|
||||
<HoppSmartAnchor
|
||||
class="link"
|
||||
to="/profile/tokens"
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
:label="t('mock_server.create_token_here')"
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,6 +179,7 @@ import IconCheck from "~icons/lucide/check"
|
|||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import IconSquare from "~icons/lucide/square"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
|
|
|
|||
202
packages/hoppscotch-common/src/components/profile/index.vue
Normal file
202
packages/hoppscotch-common/src/components/profile/index.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<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="py-4">
|
||||
<label for="displayName">
|
||||
{{ t("settings.profile_name") }}
|
||||
</label>
|
||||
<HoppSmartInput
|
||||
v-model="displayName"
|
||||
:autofocus="false"
|
||||
styles="mt-2 md:max-w-sm"
|
||||
:placeholder="`${t('settings.profile_name')}`"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="min-w-[4rem] ml-2"
|
||||
type="submit"
|
||||
:loading="updatingDisplayName"
|
||||
@click="updateDisplayName"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</div>
|
||||
<div class="py-4">
|
||||
<label for="emailAddress">
|
||||
{{ t("settings.profile_email") }}
|
||||
</label>
|
||||
<HoppSmartInput
|
||||
v-model="emailAddress"
|
||||
:autofocus="false"
|
||||
styles="flex mt-2 md:max-w-sm"
|
||||
:placeholder="`${t('settings.profile_email')}`"
|
||||
:disabled="!isEmailEditable"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
v-if="isEmailEditable"
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="min-w-[4rem] ml-2"
|
||||
type="submit"
|
||||
:loading="updatingEmailAddress"
|
||||
@click="updateEmailAddress"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</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="space-y-4 py-4">
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="SYNC_COLLECTIONS"
|
||||
@change="toggleSetting('syncCollections')"
|
||||
>
|
||||
{{ t("settings.sync_collections") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="SYNC_ENVIRONMENTS"
|
||||
@change="toggleSetting('syncEnvironments')"
|
||||
>
|
||||
{{ t("settings.sync_environments") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="SYNC_HISTORY"
|
||||
@change="toggleSetting('syncHistory')"
|
||||
>
|
||||
{{ t("settings.sync_history") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template v-if="platform.ui?.additionalProfileSections?.length">
|
||||
<template
|
||||
v-for="(item, index) in platform.ui?.additionalProfileSections"
|
||||
:key="index"
|
||||
>
|
||||
<component :is="item" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<ProfileUserDelete />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as E from "fp-ts/Either"
|
||||
import { computed, ref, watchEffect } from "vue"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
import { toggleSetting } from "~/newstore/settings"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const SYNC_COLLECTIONS = useSetting("syncCollections")
|
||||
const SYNC_ENVIRONMENTS = useSetting("syncEnvironments")
|
||||
const SYNC_HISTORY = useSetting("syncHistory")
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const isEmailEditable = computed(() => {
|
||||
return platform.auth.isEmailEditable ?? false
|
||||
})
|
||||
|
||||
const displayName = ref(currentUser.value?.displayName || "")
|
||||
const updatingDisplayName = ref(false)
|
||||
watchEffect(() => (displayName.value = currentUser.value?.displayName || ""))
|
||||
|
||||
const updateDisplayName = async () => {
|
||||
const inputName = displayName.value.trim()
|
||||
if (!inputName) {
|
||||
toast.error(`${t("error.empty_profile_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentUser.value?.displayName === inputName) {
|
||||
toast.error(`${t("error.same_profile_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
updatingDisplayName.value = true
|
||||
|
||||
const res = await platform.auth.setDisplayName(inputName)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
} else if (E.isRight(res)) {
|
||||
toast.success(`${t("profile.updated")}`)
|
||||
}
|
||||
|
||||
updatingDisplayName.value = false
|
||||
}
|
||||
|
||||
const emailAddress = ref(currentUser.value?.email || "")
|
||||
const updatingEmailAddress = ref(false)
|
||||
watchEffect(() => (emailAddress.value = currentUser.value?.email || ""))
|
||||
|
||||
const updateEmailAddress = async () => {
|
||||
const inputEmailAddress = emailAddress.value.trim()
|
||||
if (!inputEmailAddress) {
|
||||
toast.error(`${t("error.empty_email_address")}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentUser.value?.email === inputEmailAddress) {
|
||||
toast.error(`${t("error.same_email_address")}`)
|
||||
return
|
||||
}
|
||||
|
||||
updatingEmailAddress.value = true
|
||||
|
||||
const result = await platform.auth.setEmailAddress(inputEmailAddress)
|
||||
|
||||
if (!result) {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
updatingEmailAddress.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (result.type === "success") {
|
||||
toast.success(`${t("profile.verified_email_sent")}`)
|
||||
} else if (result.type === "email-already-in-use") {
|
||||
toast.error(`${t("error.email_already_exists")}`)
|
||||
} else if (result.type === "requires-recent-login") {
|
||||
await result.link()
|
||||
} else {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
updatingEmailAddress.value = false
|
||||
}
|
||||
</script>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<Teams :modal="true" />
|
||||
<TeamsView :modal="true" />
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -71,126 +71,14 @@
|
|||
<FirebaseLogout outline />
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedProfileTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab id="sync" :label="t('settings.account')">
|
||||
<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="py-4">
|
||||
<label for="displayName">
|
||||
{{ t("settings.profile_name") }}
|
||||
</label>
|
||||
<HoppSmartInput
|
||||
v-model="displayName"
|
||||
:autofocus="false"
|
||||
styles="mt-2 md:max-w-sm"
|
||||
:placeholder="`${t('settings.profile_name')}`"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="min-w-[4rem] ml-2"
|
||||
type="submit"
|
||||
:loading="updatingDisplayName"
|
||||
@click="updateDisplayName"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</div>
|
||||
<div class="py-4">
|
||||
<label for="emailAddress">
|
||||
{{ t("settings.profile_email") }}
|
||||
</label>
|
||||
<HoppSmartInput
|
||||
v-model="emailAddress"
|
||||
:autofocus="false"
|
||||
styles="flex mt-2 md:max-w-sm"
|
||||
:placeholder="`${t('settings.profile_email')}`"
|
||||
:disabled="!isEmailEditable"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonSecondary
|
||||
v-if="isEmailEditable"
|
||||
filled
|
||||
outline
|
||||
:label="t('action.save')"
|
||||
class="min-w-[4rem] ml-2"
|
||||
type="submit"
|
||||
:loading="updatingEmailAddress"
|
||||
@click="updateEmailAddress"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
</div>
|
||||
</section>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<TabsNav
|
||||
:items="PROFILE_NAVIGATION"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10"
|
||||
/>
|
||||
|
||||
<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="space-y-4 py-4">
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="SYNC_COLLECTIONS"
|
||||
@change="toggleSetting('syncCollections')"
|
||||
>
|
||||
{{ t("settings.sync_collections") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="SYNC_ENVIRONMENTS"
|
||||
@change="toggleSetting('syncEnvironments')"
|
||||
>
|
||||
{{ t("settings.sync_environments") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="SYNC_HISTORY"
|
||||
@change="toggleSetting('syncHistory')"
|
||||
>
|
||||
{{ t("settings.sync_history") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template v-if="platform.ui?.additionalProfileSections?.length">
|
||||
<template
|
||||
v-for="item in platform.ui?.additionalProfileSections"
|
||||
:key="item.id"
|
||||
>
|
||||
<component :is="item" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<ProfileUserDelete />
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab id="teams" :label="t('team.title')">
|
||||
<Teams :modal="false" class="p-4" />
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab id="tokens" :label="t('access_tokens.tab_title')">
|
||||
<AccessTokens />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -198,27 +86,21 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as E from "fp-ts/Either"
|
||||
import { computed, ref, watchEffect } from "vue"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
|
||||
import { usePageHead } from "@composables/head"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
import { toggleSetting } from "~/newstore/settings"
|
||||
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
import IconVerified from "~icons/lucide/verified"
|
||||
|
||||
type ProfileTabs = "sync" | "teams"
|
||||
|
||||
const selectedProfileTab = ref<ProfileTabs>("sync")
|
||||
import TabsNav from "~/components/TabsNav.vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
|
@ -228,9 +110,6 @@ usePageHead({
|
|||
title: computed(() => t("navigation.profile")),
|
||||
})
|
||||
|
||||
const SYNC_COLLECTIONS = useSetting("syncCollections")
|
||||
const SYNC_ENVIRONMENTS = useSetting("syncEnvironments")
|
||||
const SYNC_HISTORY = useSetting("syncHistory")
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
|
|
@ -240,10 +119,6 @@ const probableUser = useReadonlyStream(
|
|||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const isEmailEditable = computed(() => {
|
||||
return platform.auth.isEmailEditable ?? false
|
||||
})
|
||||
|
||||
const loadingCurrentUser = computed(() => {
|
||||
if (!probableUser.value) return false
|
||||
else if (!currentUser.value) return true
|
||||
|
|
@ -251,72 +126,13 @@ const loadingCurrentUser = computed(() => {
|
|||
})
|
||||
|
||||
const displayName = ref(currentUser.value?.displayName || "")
|
||||
const updatingDisplayName = ref(false)
|
||||
|
||||
watchEffect(() => (displayName.value = currentUser.value?.displayName || ""))
|
||||
|
||||
const updateDisplayName = async () => {
|
||||
const inputName = displayName.value.trim()
|
||||
if (!inputName) {
|
||||
toast.error(`${t("error.empty_profile_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentUser.value?.displayName === inputName) {
|
||||
toast.error(`${t("error.same_profile_name")}`)
|
||||
return
|
||||
}
|
||||
|
||||
updatingDisplayName.value = true
|
||||
|
||||
const res = await platform.auth.setDisplayName(inputName)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
} else if (E.isRight(res)) {
|
||||
toast.success(`${t("profile.updated")}`)
|
||||
}
|
||||
|
||||
updatingDisplayName.value = false
|
||||
}
|
||||
|
||||
const emailAddress = ref(currentUser.value?.email || "")
|
||||
const updatingEmailAddress = ref(false)
|
||||
|
||||
watchEffect(() => (emailAddress.value = currentUser.value?.email || ""))
|
||||
|
||||
const updateEmailAddress = async () => {
|
||||
const inputEmailAddress = emailAddress.value.trim()
|
||||
if (!inputEmailAddress) {
|
||||
toast.error(`${t("error.empty_email_address")}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (currentUser.value?.email === inputEmailAddress) {
|
||||
toast.error(`${t("error.same_email_address")}`)
|
||||
return
|
||||
}
|
||||
|
||||
updatingEmailAddress.value = true
|
||||
|
||||
const result = await platform.auth.setEmailAddress(inputEmailAddress)
|
||||
|
||||
if (!result) {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
updatingEmailAddress.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (result.type === "success") {
|
||||
toast.success(`${t("profile.verified_email_sent")}`)
|
||||
} else if (result.type === "email-already-in-use") {
|
||||
toast.error(`${t("error.email_already_exists")}`)
|
||||
} else if (result.type === "requires-recent-login") {
|
||||
await result.link()
|
||||
} else {
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
}
|
||||
updatingEmailAddress.value = false
|
||||
}
|
||||
|
||||
const verifyingEmailAddress = ref(false)
|
||||
|
||||
const sendEmailVerification = () => {
|
||||
|
|
@ -333,4 +149,23 @@ const sendEmailVerification = () => {
|
|||
verifyingEmailAddress.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const PROFILE_NAVIGATION = computed(() => [
|
||||
{
|
||||
route: "/profile",
|
||||
label: t("settings.account"),
|
||||
icon: null,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
route: "/profile/teams",
|
||||
label: t("team.title"),
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
route: "/profile/tokens",
|
||||
label: t("access_tokens.tab_title"),
|
||||
icon: null,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
|
|
|||
5
packages/hoppscotch-common/src/pages/profile/index.vue
Normal file
5
packages/hoppscotch-common/src/pages/profile/index.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<Profile />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
5
packages/hoppscotch-common/src/pages/profile/teams.vue
Normal file
5
packages/hoppscotch-common/src/pages/profile/teams.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<TeamsView :modal="false" class="p-4" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
5
packages/hoppscotch-common/src/pages/profile/tokens.vue
Normal file
5
packages/hoppscotch-common/src/pages/profile/tokens.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<AccessTokens />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
Loading…
Reference in a new issue