Skip to content

Commit

Permalink
Merge pull request #208 from bcgov/multi-invite
Browse files Browse the repository at this point in the history
Use vee-validate for Invite form; multi-user invites
  • Loading branch information
norrisng-bc authored Jun 4, 2024
2 parents 8fd0c09 + 9a3ea0d commit 6bd2898
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 179 deletions.
3 changes: 2 additions & 1 deletion frontend/src/components/bucket/BucketChildConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ const onCancel = () => {
name="subKey"
label="Path"
placeholder="my-documents"
:help-text="`The relative path of the subfolder. You can pick a new path or choose an existing object storage path,
:help-text="`The relative path of the subfolder.
You can pick a new path or choose an existing object storage path,
but it can't be changed after it is set.<br />
Folder levels are supported using '/' between levels (for example: 2024/January/my-documents).`"
class="child-input"
Expand Down
312 changes: 167 additions & 145 deletions frontend/src/components/common/Invite.vue
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';
Expand All @@ -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,
Expand All @@ -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"
Expand All @@ -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>
Loading

0 comments on commit 6bd2898

Please sign in to comment.