feat: copyable invite links (#4153)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -1009,7 +1009,10 @@
|
|||||||
"success_invites": "Success invites",
|
"success_invites": "Success invites",
|
||||||
"title": "Workspaces",
|
"title": "Workspaces",
|
||||||
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
"we_sent_invite_link": "We sent an invite link to all invitees!",
|
||||||
|
"invite_sent_smtp_disabled": "Invite links generated",
|
||||||
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the workspace.",
|
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the workspace.",
|
||||||
|
"invite_sent_smtp_disabled_description": "Sending invite emails is disabled for this instance of Hoppscotch. Please use the Copy link button to copy and share the invite link manually.",
|
||||||
|
"copy_invite_link": "Copy Invite Link",
|
||||||
"search_title": "Team Requests"
|
"search_title": "Team Requests"
|
||||||
},
|
},
|
||||||
"team_environment": {
|
"team_environment": {
|
||||||
|
|||||||
@@ -10,10 +10,18 @@
|
|||||||
<div class="mb-8 flex max-w-md flex-col items-center justify-center">
|
<div class="mb-8 flex max-w-md flex-col items-center justify-center">
|
||||||
<icon-lucide-users class="h-6 w-6 text-accent" />
|
<icon-lucide-users class="h-6 w-6 text-accent" />
|
||||||
<h3 class="my-2 text-center text-lg">
|
<h3 class="my-2 text-center text-lg">
|
||||||
{{ t("team.we_sent_invite_link") }}
|
{{
|
||||||
|
inviteMethod === "email"
|
||||||
|
? t("team.we_sent_invite_link")
|
||||||
|
: t("team.invite_sent_smtp_disabled")
|
||||||
|
}}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
{{ t("team.we_sent_invite_link_description") }}
|
{{
|
||||||
|
inviteMethod === "email"
|
||||||
|
? t("team.we_sent_invite_link_description")
|
||||||
|
: t("team.invite_sent_smtp_disabled_description")
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="successInvites.length">
|
<div v-if="successInvites.length">
|
||||||
@@ -33,6 +41,20 @@
|
|||||||
class="svg-icons mr-4 text-green-500"
|
class="svg-icons mr-4 text-green-500"
|
||||||
/>
|
/>
|
||||||
<span class="truncate">{{ invitee.email }}</span>
|
<span class="truncate">{{ invitee.email }}</span>
|
||||||
|
<span class="flex items-center gap-1 ml-auto">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
:icon="getCopyIcon(invitee.invitationID).value"
|
||||||
|
class="rounded-md"
|
||||||
|
:label="t('team.copy_invite_link')"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
copyInviteLink(invitee.invitationID)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,6 +129,20 @@
|
|||||||
:value="invitee.inviteeRole"
|
:value="invitee.inviteeRole"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
outline
|
||||||
|
:icon="getCopyIcon(invitee.id).value"
|
||||||
|
class="rounded-md"
|
||||||
|
:title="t('team.copy_invite_link')"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
copyInviteLink(invitee.id)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -352,7 +388,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch, ref, reactive, computed } from "vue"
|
import { watch, ref, reactive, computed, Ref, onMounted } from "vue"
|
||||||
import * as T from "fp-ts/Task"
|
import * as T from "fp-ts/Task"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -386,7 +422,24 @@ import IconMailCheck from "~icons/lucide/mail-check"
|
|||||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||||
import IconCircle from "~icons/lucide/circle"
|
import IconCircle from "~icons/lucide/circle"
|
||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconCheck from "~icons/lucide/check"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
import { refAutoReset } from "@vueuse/core"
|
||||||
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
|
const copyIcons: Record<string, Ref<typeof IconCopy | typeof IconCheck>> = {}
|
||||||
|
const getCopyIcon = (id: string) => {
|
||||||
|
if (!copyIcons[id]) {
|
||||||
|
copyIcons[id] = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||||
|
IconCopy,
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyIcons[id]
|
||||||
|
}
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -406,6 +459,20 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const inviteMethod = ref<"email" | "link">("email")
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const getIsSMTPEnabled = platform.infra?.getIsSMTPEnabled
|
||||||
|
|
||||||
|
if (getIsSMTPEnabled) {
|
||||||
|
const res = await getIsSMTPEnabled()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
inviteMethod.value = res.right ? "email" : "link"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const pendingInvites = useGQLQuery<
|
const pendingInvites = useGQLQuery<
|
||||||
GetPendingInvitesQuery,
|
GetPendingInvitesQuery,
|
||||||
GetPendingInvitesQueryVariables,
|
GetPendingInvitesQueryVariables,
|
||||||
@@ -496,6 +563,14 @@ const removeNewInvitee = (id: number) => {
|
|||||||
newInvites.value.splice(id, 1)
|
newInvites.value.splice(id, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyInviteLink = (invitationID: string) => {
|
||||||
|
copyToClipboard(
|
||||||
|
`${import.meta.env.VITE_BASE_URL}/join-team?id=${invitationID}`
|
||||||
|
)
|
||||||
|
|
||||||
|
getCopyIcon(invitationID).value = IconCheck
|
||||||
|
}
|
||||||
|
|
||||||
type SendInvitesErrorType =
|
type SendInvitesErrorType =
|
||||||
| {
|
| {
|
||||||
email: Email
|
email: Email
|
||||||
@@ -505,6 +580,7 @@ type SendInvitesErrorType =
|
|||||||
| {
|
| {
|
||||||
email: Email
|
email: Email
|
||||||
status: "success"
|
status: "success"
|
||||||
|
invitationID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendInvitesResult = ref<Array<SendInvitesErrorType>>([])
|
const sendInvitesResult = ref<Array<SendInvitesErrorType>>([])
|
||||||
@@ -555,9 +631,10 @@ const sendInvites = async () => {
|
|||||||
email: newInvites.value[i].key as Email,
|
email: newInvites.value[i].key as Email,
|
||||||
error: err,
|
error: err,
|
||||||
}),
|
}),
|
||||||
() => ({
|
(invitation) => ({
|
||||||
status: "success" as const,
|
status: "success" as const,
|
||||||
email: newInvites.value[i].key as Email,
|
email: newInvites.value[i].key as Email,
|
||||||
|
invitationID: invitation.id,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { InspectorsPlatformDef } from "./inspectors"
|
|||||||
import { ServiceClassInstance } from "dioc"
|
import { ServiceClassInstance } from "dioc"
|
||||||
import { IOPlatformDef } from "./io"
|
import { IOPlatformDef } from "./io"
|
||||||
import { SpotlightPlatformDef } from "./spotlight"
|
import { SpotlightPlatformDef } from "./spotlight"
|
||||||
|
import { InfraPlatformDef } from "./infra"
|
||||||
import { Ref } from "vue"
|
import { Ref } from "vue"
|
||||||
|
|
||||||
export type PlatformDef = {
|
export type PlatformDef = {
|
||||||
@@ -52,6 +53,7 @@ export type PlatformDef = {
|
|||||||
*/
|
*/
|
||||||
workspaceSwitcherLogin?: Ref<boolean>
|
workspaceSwitcherLogin?: Ref<boolean>
|
||||||
}
|
}
|
||||||
|
infra?: InfraPlatformDef
|
||||||
}
|
}
|
||||||
|
|
||||||
export let platform: PlatformDef
|
export let platform: PlatformDef
|
||||||
|
|||||||
5
packages/hoppscotch-common/src/platform/infra.ts
Normal file
5
packages/hoppscotch-common/src/platform/infra.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
|
||||||
|
export type InfraPlatformDef = {
|
||||||
|
getIsSMTPEnabled?: () => Promise<E.Either<string, boolean>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
query GetSMTPStatus {
|
||||||
|
isSMTPEnabled
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { ExtensionInterceptorService } from "@hoppscotch/common/platform/std/int
|
|||||||
import { stdFooterItems } from "@hoppscotch/common/platform/std/ui/footerItem"
|
import { stdFooterItems } from "@hoppscotch/common/platform/std/ui/footerItem"
|
||||||
import { stdSupportOptionItems } from "@hoppscotch/common/platform/std/ui/supportOptionsItem"
|
import { stdSupportOptionItems } from "@hoppscotch/common/platform/std/ui/supportOptionsItem"
|
||||||
import { browserIODef } from "@hoppscotch/common/platform/std/io"
|
import { browserIODef } from "@hoppscotch/common/platform/std/io"
|
||||||
|
import { InfraPlatform } from "@platform/infra/infra.platform"
|
||||||
|
|
||||||
createHoppApp("#app", {
|
createHoppApp("#app", {
|
||||||
ui: {
|
ui: {
|
||||||
@@ -40,4 +41,5 @@ createHoppApp("#app", {
|
|||||||
exportAsGIST: false,
|
exportAsGIST: false,
|
||||||
hasTelemetry: false,
|
hasTelemetry: false,
|
||||||
},
|
},
|
||||||
|
infra: InfraPlatform,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { runGQLQuery } from "@hoppscotch/common/helpers/backend/GQLClient"
|
||||||
|
import { InfraPlatformDef } from "@hoppscotch/common/platform/infra"
|
||||||
|
import { GetSmtpStatusDocument } from "../../api/generated/graphql"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
|
||||||
|
const getSMTPStatus = () => {
|
||||||
|
return runGQLQuery({
|
||||||
|
query: GetSmtpStatusDocument,
|
||||||
|
variables: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfraPlatform: InfraPlatformDef = {
|
||||||
|
getIsSMTPEnabled: async () => {
|
||||||
|
const res = await getSMTPStatus()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
return E.right(res.right.isSMTPEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.left("SMTP_STATUS_FETCH_FAILED")
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user