-
Notifications
You must be signed in to change notification settings - Fork 173
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: Allow users to manage emails (#2613)
- Loading branch information
Showing
11 changed files
with
551 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
<template> | ||
<section> | ||
<div class="md:max-w-xl md:mx-auto"> | ||
<SettingsSectionHeader | ||
title="Emails addresses" | ||
text="Manage your email addresses" | ||
/> | ||
<SettingsSectionHeader title="Your emails" subheading /> | ||
<SettingsUserEmailList class="pt-6" :email-data="emailItems" /> | ||
<hr class="my-6 md:my-10" /> | ||
<SettingsSectionHeader title="Add new email" subheading /> | ||
<div class="flex flex-col md:flex-row w-full pt-4 md:pt-6 pb-6"> | ||
<div class="flex flex-col md:flex-row gap-x-2 w-full"> | ||
<FormTextInput | ||
v-model="email" | ||
color="foundation" | ||
label-position="left" | ||
label="Email address" | ||
name="email" | ||
:rules="[isEmail, isRequired]" | ||
placeholder="Email address" | ||
show-label | ||
wrapper-classes="flex-1 py-3 md:py-0 w-full" | ||
/> | ||
<FormButton @click="onAddEmailSubmit">Add</FormButton> | ||
</div> | ||
</div> | ||
</div> | ||
</section> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { orderBy } from 'lodash-es' | ||
import { graphql } from '~~/lib/common/generated/gql' | ||
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast' | ||
import { isEmail, isRequired } from '~~/lib/common/helpers/validation' | ||
import { useForm } from 'vee-validate' | ||
import { useQuery, useMutation } from '@vue/apollo-composable' | ||
import { settingsUserEmailsQuery } from '~/lib/settings/graphql/queries' | ||
import { settingsCreateUserEmailMutation } from '~/lib/settings/graphql/mutations' | ||
import { | ||
getFirstErrorMessage, | ||
convertThrowIntoFetchResult | ||
} from '~~/lib/common/helpers/graphql' | ||
graphql(` | ||
fragment SettingsUserEmails_User on User { | ||
id | ||
emails { | ||
...SettingsUserEmailCards_UserEmail | ||
} | ||
} | ||
`) | ||
type FormValues = { email: string } | ||
const { handleSubmit } = useForm<FormValues>() | ||
const { triggerNotification } = useGlobalToast() | ||
const { result: userEmailsResult } = useQuery(settingsUserEmailsQuery) | ||
const { mutate: createMutation } = useMutation(settingsCreateUserEmailMutation) | ||
const email = ref('') | ||
// Mak sure primary email is always on top, followed by verified emails | ||
const emailItems = computed(() => | ||
userEmailsResult.value?.activeUser?.emails | ||
? orderBy( | ||
userEmailsResult.value?.activeUser.emails, | ||
['primary', 'verified'], | ||
['desc', 'desc'] | ||
) | ||
: [] | ||
) | ||
const onAddEmailSubmit = handleSubmit(async () => { | ||
const result = await createMutation({ input: { email: email.value } }).catch( | ||
convertThrowIntoFetchResult | ||
) | ||
if (result?.data) { | ||
triggerNotification({ | ||
type: ToastNotificationType.Success, | ||
title: `${email.value} added` | ||
}) | ||
} else { | ||
const errorMessage = getFirstErrorMessage(result?.errors) | ||
triggerNotification({ | ||
type: ToastNotificationType.Danger, | ||
title: errorMessage | ||
}) | ||
} | ||
}) | ||
</script> |
71 changes: 71 additions & 0 deletions
71
packages/frontend-2/components/settings/user/email/DeleteDialog.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<template> | ||
<LayoutDialog | ||
v-model:open="isOpen" | ||
title="Delete email address" | ||
max-width="sm" | ||
:buttons="dialogButtons" | ||
> | ||
<p class="text-body-xs text-foreground"> | ||
Are you sure you want to delete | ||
<span class="font-medium">{{ email }}</span> | ||
from your account? | ||
</p> | ||
</LayoutDialog> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import type { LayoutDialogButton } from '@speckle/ui-components' | ||
import { settingsDeleteUserEmailMutation } from '~/lib/settings/graphql/mutations' | ||
import { useMutation } from '@vue/apollo-composable' | ||
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast' | ||
import { | ||
getFirstErrorMessage, | ||
convertThrowIntoFetchResult | ||
} from '~~/lib/common/helpers/graphql' | ||
const props = defineProps<{ | ||
emailId: string | ||
email: string | ||
}>() | ||
const isOpen = defineModel<boolean>('open', { required: true }) | ||
const { mutate: deleteMutation } = useMutation(settingsDeleteUserEmailMutation) | ||
const { triggerNotification } = useGlobalToast() | ||
const dialogButtons = computed((): LayoutDialogButton[] => [ | ||
{ | ||
text: 'Cancel', | ||
props: { color: 'outline', fullWidth: true }, | ||
onClick: () => { | ||
isOpen.value = false | ||
} | ||
}, | ||
{ | ||
text: 'Delete', | ||
props: { color: 'primary', fullWidth: true }, | ||
onClick: () => { | ||
onDeleteEmail() | ||
} | ||
} | ||
]) | ||
const onDeleteEmail = async () => { | ||
const result = await deleteMutation({ input: { id: props.emailId } }).catch( | ||
convertThrowIntoFetchResult | ||
) | ||
if (result?.data) { | ||
triggerNotification({ | ||
type: ToastNotificationType.Success, | ||
title: `${props.email} deleted` | ||
}) | ||
} else { | ||
const errorMessage = getFirstErrorMessage(result?.errors) | ||
triggerNotification({ | ||
type: ToastNotificationType.Danger, | ||
title: errorMessage | ||
}) | ||
} | ||
isOpen.value = false | ||
} | ||
</script> |
17 changes: 17 additions & 0 deletions
17
packages/frontend-2/components/settings/user/email/List.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<template> | ||
<ul class="flex flex-col"> | ||
<SettingsUserEmailListItem | ||
v-for="email in emailData" | ||
:key="email.id" | ||
:email-data="email" | ||
/> | ||
</ul> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import type { SettingsUserEmailCards_UserEmailFragment } from '~~/lib/common/generated/gql/graphql' | ||
defineProps<{ | ||
emailData: SettingsUserEmailCards_UserEmailFragment[] | ||
}>() | ||
</script> |
138 changes: 138 additions & 0 deletions
138
packages/frontend-2/components/settings/user/email/ListItem.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
<template> | ||
<li class="border-x border-b first:border-t first:rounded-t-lg last:rounded-b-lg p-6"> | ||
<div | ||
v-if="emailData.primary || !emailData.verified" | ||
class="flex w-full gap-x-2 pb-4 md:pb-3" | ||
> | ||
<CommonBadge | ||
v-if="emailData.primary" | ||
rounded | ||
color-classes="bg-info-lighter text-outline-4" | ||
> | ||
Primary | ||
</CommonBadge> | ||
<CommonBadge | ||
v-if="!emailData.verified" | ||
rounded | ||
color-classes="bg-outline-3 text-foreground-3" | ||
> | ||
Unverified | ||
</CommonBadge> | ||
<FormButton | ||
v-if="!emailData.verified" | ||
color="outline" | ||
size="sm" | ||
@click="resendVerificationEmail" | ||
> | ||
Resend verification email | ||
</FormButton> | ||
</div> | ||
<div class="flex flex-col md:flex-row"> | ||
<div class="flex-1"> | ||
<p class="text-body-xs font-medium text-foreground md:pt-0.5"> | ||
{{ emailData.email }} | ||
</p> | ||
<p v-if="description" class="text-body-2xs pt-1 text-foreground-2"> | ||
{{ description }} | ||
</p> | ||
</div> | ||
<div class="flex gap-x-2 pt-4 md:pt-0"> | ||
<FormButton | ||
:disabled="!emailData.verified || emailData.primary" | ||
color="outline" | ||
size="sm" | ||
@click="toggleSetPrimaryDialog" | ||
> | ||
Set as primary | ||
</FormButton> | ||
<FormButton | ||
:disabled="emailData.primary" | ||
color="outline" | ||
size="sm" | ||
@click="toggleDeleteDialog" | ||
> | ||
Delete | ||
</FormButton> | ||
</div> | ||
</div> | ||
|
||
<SettingsUserEmailSetPrimaryDialog | ||
v-model:open="showSetPrimaryDialog" | ||
:email-id="emailData.id" | ||
:email="emailData.email" | ||
/> | ||
|
||
<SettingsUserEmailDeleteDialog | ||
v-model:open="showDeleteDialog" | ||
:email-id="emailData.id" | ||
:email="emailData.email" | ||
/> | ||
</li> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import type { SettingsUserEmailCards_UserEmailFragment } from '~~/lib/common/generated/gql/graphql' | ||
import { useGlobalToast, ToastNotificationType } from '~~/lib/common/composables/toast' | ||
import { graphql } from '~~/lib/common/generated/gql' | ||
import { useMutation } from '@vue/apollo-composable' | ||
import { settingsNewEmailVerificationMutation } from '~~/lib/settings/graphql/mutations' | ||
import { | ||
getFirstErrorMessage, | ||
convertThrowIntoFetchResult | ||
} from '~~/lib/common/helpers/graphql' | ||
graphql(` | ||
fragment SettingsUserEmailCards_UserEmail on UserEmail { | ||
id | ||
primary | ||
verified | ||
} | ||
`) | ||
const props = defineProps<{ | ||
emailData: SettingsUserEmailCards_UserEmailFragment | ||
}>() | ||
const { triggerNotification } = useGlobalToast() | ||
const { mutate: resendMutation } = useMutation(settingsNewEmailVerificationMutation) | ||
const showDeleteDialog = ref(false) | ||
const showSetPrimaryDialog = ref(false) | ||
const description = computed(() => { | ||
if (props.emailData.primary) { | ||
return 'Used for sign in and notifications' | ||
} else if (!props.emailData.verified) { | ||
return 'Unverified email cannot be set as primary' | ||
} | ||
return null | ||
}) | ||
const toggleSetPrimaryDialog = () => { | ||
showSetPrimaryDialog.value = true | ||
} | ||
const toggleDeleteDialog = () => { | ||
showDeleteDialog.value = true | ||
} | ||
const resendVerificationEmail = async () => { | ||
const result = await resendMutation({ input: { id: props.emailData.id } }).catch( | ||
convertThrowIntoFetchResult | ||
) | ||
if (result?.data) { | ||
triggerNotification({ | ||
type: ToastNotificationType.Success, | ||
title: `Verification mail sent to ${props.emailData.email}` | ||
}) | ||
} else { | ||
const errorMessage = getFirstErrorMessage(result?.errors) | ||
triggerNotification({ | ||
type: ToastNotificationType.Danger, | ||
title: errorMessage | ||
}) | ||
} | ||
} | ||
</script> |
Oops, something went wrong.