-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #208 from bcgov/multi-invite
Use vee-validate for Invite form; multi-user invites
- Loading branch information
Showing
7 changed files
with
199 additions
and
179 deletions.
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 |
---|---|---|
@@ -1,18 +1,11 @@ | ||
<script setup lang="ts"> | ||
import { computed, ref, } from 'vue'; | ||
import { computed, ref } from 'vue'; | ||
import { storeToRefs } from 'pinia'; | ||
import { Form } from 'vee-validate'; | ||
import { object, string } from 'yup'; | ||
import { useForm, ErrorMessage } from 'vee-validate'; | ||
import * as yup from 'yup'; | ||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; | ||
import { | ||
Button, | ||
RadioButton, | ||
Checkbox, | ||
useToast, | ||
InputSwitch | ||
} from '@/lib/primevue'; | ||
import { Button, RadioButton, Checkbox, useToast, TextArea } from '@/lib/primevue'; | ||
import TextInput from '@/components/form/TextInput.vue'; | ||
import { Share } from '@/components/common'; | ||
import { Spinner } from '@/components/layout'; | ||
import { Regex } from '@/utils/constants'; | ||
|
@@ -38,8 +31,6 @@ const toast = useToast(); | |
// State | ||
const inviteLoading: Ref<boolean> = ref(false); | ||
const showInviteLink: Ref<boolean> = ref(false); | ||
const inviteLink: Ref<string> = ref(''); | ||
const timeFrames: Record<string, number> = { | ||
'1 Hour': 3600, | ||
|
@@ -60,150 +51,195 @@ const objectPermCodes: Record<string, string> = { | |
UPDATE: 'Update' | ||
}; | ||
const formData: Ref<any> = ref({ | ||
expiresAt: 86400, | ||
isRestricted: false, | ||
permCodes: ['READ'], | ||
}); | ||
// Permissions selection | ||
const selectedOptions = computed(() => { | ||
return props.resourceType === 'bucket' ? bucketPermCodes : objectPermCodes; | ||
}); | ||
const isOptionUnselectable = (optionName: string) => { | ||
// Make default permission disabled | ||
return optionName === 'Read'; | ||
}; | ||
// Form validation schema | ||
const schema = yup.object().shape({ | ||
email: yup | ||
.string() | ||
.matches(new RegExp(Regex.EMAIL), 'Provide a valid email address') | ||
.when('emailType', { | ||
is: (string: string) => string === 'single', | ||
then: (schema) => schema.required('Email address is required') | ||
}), | ||
multiEmail: yup | ||
.array() | ||
.transform(function (value, originalValue) { | ||
if (this.isType(value) && value !== null) return value; | ||
return originalValue ? originalValue.split(/[\r\n ,;]+/).filter((item: string) => item) : []; | ||
}) | ||
.of( | ||
yup | ||
.string() | ||
.matches(new RegExp(Regex.EMAIL), 'Provide a list of valid email addresses separated by commas or semicolons') | ||
) | ||
.when('emailType', { | ||
is: (string: string) => string === 'multi', | ||
then: (schema) => schema.min(1, 'Enter one or more email addresses') | ||
}) | ||
}); | ||
// Form validation | ||
const schema = object({ | ||
// TODO: conditional validation | ||
// isRestricted: boolean(), | ||
// email: string() | ||
// .email() | ||
// .matches(new RegExp(Regex.EMAIL), 'Provide a valid email address') | ||
// .when('isRestricted', { | ||
// is: true, | ||
// then: (schema) => schema | ||
// .required('Email address is required') | ||
// }) | ||
email: string().matches(new RegExp(Regex.EMAIL), 'Provide a valid email address') | ||
// create a vee-validate form context | ||
const { values, defineField, handleSubmit } = useForm({ | ||
validationSchema: schema, | ||
initialValues: { | ||
expiresAt: 86400, | ||
permCodes: ['READ'], | ||
email: '', | ||
emailType: 'single', | ||
multiEmail: '' | ||
} | ||
}); | ||
// maps the input models for vee-validate | ||
const [expiresAt] = defineField('expiresAt', {}); | ||
const [permCodes] = defineField('permCodes', {}); | ||
const [emailType] = defineField('emailType', {}); | ||
const [email] = defineField('email', {}); | ||
const [multiEmail] = defineField('multiEmail', {}); | ||
//Action | ||
async function invite(data: any) { | ||
// Invite form is submitted | ||
const onSubmit = handleSubmit(async (values: any) => { | ||
inviteLoading.value = true; | ||
try { | ||
// set expiry date | ||
const expiresAt = Math.floor(Date.now() / 1000) + formData.value.expiresAt; | ||
// put input email addresses into an array | ||
// NOTE: emails are coming from `data` | ||
const emails = formData.value.isRestricted ? [data.email] : []; | ||
const expiresAt = Math.floor(Date.now() / 1000) + values.expiresAt; | ||
// put email(s) into an array | ||
let emailArray; | ||
if (values.emailType === 'single') emailArray = [values.email]; | ||
// for list of emails, delimit, de-dupe and remove empty | ||
else emailArray = [...new Set(values.multiEmail.split(/[\r\n ,;]+/).filter((item: string) => item))]; | ||
// TODO: add perms to users already in the system | ||
// generate invites (for emails not already in the system) | ||
const invites = await inviteService.createInvites( | ||
await inviteService.createInvites( | ||
props.resourceType, | ||
props.resource, | ||
getUser.value?.profile, | ||
emails, | ||
emailArray, | ||
expiresAt, | ||
formData.value.permCodes, // use formData because it was bound during setup | ||
values.permCodes | ||
); | ||
// if not restricting to an email, show link | ||
if(emails.length == 0) { | ||
inviteLink.value = `${window.location.origin}/invite/${invites[0].token}`; | ||
toast.success('', 'Invite link created.'); | ||
showInviteLink.value = true; | ||
} | ||
// else show email confirmation | ||
else { | ||
// TODO: output report (list of invites sent, CHES trx ID (?)) | ||
toast.success('', 'Invite notifications sent.', { life: 5000 }); | ||
showInviteLink.value = false; | ||
} | ||
// TODO: output report (list of invites sent, CHES trx ID (?)) | ||
toast.success('', 'Invite notifications sent.', { life: 5000 }); | ||
} catch (error: any) { | ||
toast.error('Creating Invite', error.response?.data.detail, { life: 0 }); | ||
} | ||
inviteLoading.value = false; | ||
} | ||
}); | ||
</script> | ||
|
||
<template> | ||
<h3 class="mt-1 mb-2">{{ (props.label) }}</h3> | ||
<!-- :initial-values="formData" --> | ||
<Form | ||
:initial-values="formData" | ||
:validation-schema="schema" | ||
@submit="invite" | ||
> | ||
<p>Make invite available for</p> | ||
<div class="flex flex-wrap gap-3"> | ||
<div | ||
v-for="(value, name) in timeFrames" | ||
:key="value" | ||
class="flex align-items-center" | ||
> | ||
<RadioButton | ||
v-model="formData.expiresAt" | ||
:input-id="value.toString()" | ||
:name="name" | ||
:value="value" | ||
/> | ||
<label | ||
:for="value.toString()" | ||
class="ml-2" | ||
<h3 class="mt-1 mb-2">{{ props.label }}</h3> | ||
<form @submit="onSubmit"> | ||
<p class="mb-2">Make invite available for</p> | ||
<div class="flex flex-wrap gap-3"> | ||
<div | ||
v-for="(value, name) in timeFrames" | ||
:key="value" | ||
class="flex align-items-center" | ||
> | ||
{{ name }} | ||
</label> | ||
<RadioButton | ||
v-model="expiresAt" | ||
name="expiresAt" | ||
:value="value" | ||
/> | ||
<label | ||
:for="value.toString()" | ||
class="ml-2" | ||
> | ||
{{ name }} | ||
</label> | ||
</div> | ||
</div> | ||
</div> | ||
<p class="mt-4 mb-2">Access options</p> | ||
<div class="flex flex-wrap gap-3"> | ||
<div | ||
v-for="(name, value) in selectedOptions" | ||
:key="value" | ||
class="flex align-items-center" | ||
> | ||
<Checkbox | ||
v-model="formData.permCodes" | ||
:input-id="value.toString()" | ||
:name="name" | ||
:value="value" | ||
:disabled="isOptionUnselectable(name)" | ||
/> | ||
<label | ||
:for="value.toString()" | ||
class="ml-2" | ||
|
||
<p class="mt-4 mb-2">Access options</p> | ||
<div class="flex flex-wrap gap-3 mb-4"> | ||
<div | ||
v-for="(name, value) in selectedOptions" | ||
:key="value" | ||
class="flex align-items-center" | ||
> | ||
{{ name }} | ||
</label> | ||
<Checkbox | ||
v-model="permCodes" | ||
name="permCodes" | ||
:value="value" | ||
:disabled="value === 'READ'" | ||
/> | ||
<label | ||
:for="value.toString()" | ||
class="ml-2" | ||
> | ||
{{ name }} | ||
</label> | ||
</div> | ||
</div> | ||
|
||
<p class="mb-2">Send to</p> | ||
<div class="flex flex-wrap gap-3 mb-3"> | ||
<div class="flex align-items-center"> | ||
<RadioButton | ||
v-model="emailType" | ||
name="emailType" | ||
value="single" | ||
/> | ||
<label | ||
for="single" | ||
class="ml-2" | ||
> | ||
Single | ||
</label> | ||
</div> | ||
<div class="flex align-items-center"> | ||
<RadioButton | ||
v-model="emailType" | ||
name="emailType" | ||
value="multi" | ||
/> | ||
<label | ||
for="multi" | ||
class="ml-2" | ||
> | ||
Multiple | ||
</label> | ||
</div> | ||
</div> | ||
|
||
<div v-if="values.emailType === 'single'"> | ||
<TextInput | ||
v-model="email" | ||
name="email" | ||
type="text" | ||
placeholder="Enter email" | ||
help-text="The Invite will be emailed to this person" | ||
class="invite-email" | ||
/> | ||
</div> | ||
<div v-else> | ||
<div class="field"> | ||
<!-- eslint-disable --> | ||
<TextArea | ||
v-model="multiEmail" | ||
name="multiEmail" | ||
type="textarea" | ||
placeholder="Enter email(s) separated by commas (, ) or semicolons (; ) - for example: [email protected], [email protected]" | ||
class="multi-email block" | ||
/> | ||
<!-- eslint-enable --> | ||
<small | ||
id="multiEmail-help" | ||
class="block" | ||
> | ||
Enter an email address for each person you are inviting to access this | ||
{{ props.resourceType === 'bucket' ? 'folder' : 'file' }}. The email address must be associated with the | ||
account they use to sign in to BCBox. | ||
</small> | ||
<ErrorMessage name="multiEmail" /> | ||
</div> | ||
</div> | ||
</div> | ||
<p class="mt-4 mb-2">Restrict to user's email</p> | ||
<!-- <p class="mt-4 mb-2">Restrict invite to a user signing in to BCBox with the following email address</p> --> | ||
<div class="flex flex-column gap-2"> | ||
<InputSwitch | ||
v-model="formData.isRestricted" | ||
name="isRestricted" | ||
class="mb-3" | ||
/> | ||
</div> | ||
<!-- if scoping invite to specific users --> | ||
<div v-if="formData.isRestricted"> | ||
<!-- v-model="formData.email" --> | ||
<TextInput | ||
v-if="formData.isRestricted" | ||
v-model="formData.email" | ||
name="email" | ||
type="email" | ||
placeholder="Enter email" | ||
help-text="The Invite will be emailed to this person" | ||
class="invite-email" | ||
/> | ||
|
||
<div class="my-4 inline-flex"> | ||
<Button | ||
class="p-button p-button-primary mr-3" | ||
|
@@ -221,28 +257,14 @@ async function invite(data: any) { | |
class="h-2rem w-2rem" | ||
/> | ||
</div> | ||
</div> | ||
<!-- else generating an open invite --> | ||
<div v-else> | ||
<Button | ||
class="p-button p-button-primary my-3 block" | ||
type="submit" | ||
> | ||
Generate invite link | ||
</Button> | ||
<Share | ||
v-if="showInviteLink" | ||
label="Invite link" | ||
:resource-type="resourceType" | ||
:resource="resource" | ||
:invite-link="inviteLink" | ||
/> | ||
</div> | ||
</Form> | ||
</form> | ||
</template> | ||
|
||
<style scoped lang="scss"> | ||
.invite-email:deep(input) { | ||
width: 80%; | ||
} | ||
.multi-email { | ||
width: 100%; | ||
} | ||
</style> |
Oops, something went wrong.