123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- <template>
- <div>
- <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
- <div class="relative">
- <input
- v-model="search"
- @keyup.esc="search = ''"
- tabindex="0"
- type="text"
- class="w-full md:w-64 appearance-none shadow bg-white text-grey-700 focus:outline-none rounded py-3 pl-3 pr-8"
- placeholder="Search Usernames"
- />
- <icon
- v-if="search"
- @click.native="search = ''"
- name="close-circle"
- class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
- />
- <icon
- v-else
- name="search"
- class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
- />
- </div>
- <div class="mt-4 md:mt-0">
- <button
- @click="addUsernameModalOpen = true"
- class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
- >
- Add Username
- </button>
- </div>
- </div>
- <vue-good-table
- v-if="initialUsernames.length"
- @on-search="debounceToolips"
- :columns="columns"
- :rows="rows"
- :search-options="{
- enabled: true,
- skipDiacritics: true,
- externalQuery: search,
- }"
- :sort-options="{
- enabled: true,
- initialSortBy: { field: 'created_at', type: 'desc' },
- }"
- styleClass="vgt-table"
- >
- <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
- No usernames found for that search!
- </div>
- <template slot="table-row" slot-scope="props">
- <span
- v-if="props.column.field == 'created_at'"
- class="tooltip outline-none text-sm"
- :data-tippy-content="props.row.created_at | formatDate"
- >{{ props.row.created_at | timeAgo }}
- </span>
- <span v-else-if="props.column.field == 'username'">
- <span
- class="tooltip cursor-pointer outline-none"
- data-tippy-content="Click to copy"
- v-clipboard="() => props.row.username"
- v-clipboard:success="clipboardSuccess"
- v-clipboard:error="clipboardError"
- >{{ props.row.username | truncate(30) }}</span
- >
- </span>
- <span v-else-if="props.column.field == 'description'">
- <div v-if="usernameIdToEdit === props.row.id" class="flex items-center">
- <input
- @keyup.enter="editUsername(rows[props.row.originalIndex])"
- @keyup.esc="usernameIdToEdit = usernameDescriptionToEdit = ''"
- v-model="usernameDescriptionToEdit"
- type="text"
- class="flex-grow appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
- :class="
- usernameDescriptionToEdit.length > 100 ? 'border-red-500' : 'border-transparent'
- "
- placeholder="Add description"
- tabindex="0"
- autofocus
- />
- <icon
- name="close"
- class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
- @click.native="usernameIdToEdit = usernameDescriptionToEdit = ''"
- />
- <icon
- name="save"
- class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
- @click.native="editUsername(rows[props.row.originalIndex])"
- />
- </div>
- <div v-else-if="props.row.description" class="flex items-centers">
- <span class="tooltip outline-none" :data-tippy-content="props.row.description">{{
- props.row.description | truncate(60)
- }}</span>
- <icon
- name="edit"
- class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer ml-2"
- @click.native="
- ;(usernameIdToEdit = props.row.id),
- (usernameDescriptionToEdit = props.row.description)
- "
- />
- </div>
- <div v-else class="flex justify-center">
- <icon
- name="plus"
- class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
- @click.native=";(usernameIdToEdit = props.row.id), (usernameDescriptionToEdit = '')"
- />
- </div>
- </span>
- <span v-else-if="props.column.field === 'default_recipient'">
- <div v-if="props.row.default_recipient">
- {{ props.row.default_recipient.email | truncate(30) }}
- <icon
- name="edit"
- class="ml-2 inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
- @click.native="openUsernameDefaultRecipientModal(props.row)"
- />
- </div>
- <div class="flex justify-center" v-else>
- <icon
- name="plus"
- class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
- @click.native="openUsernameDefaultRecipientModal(props.row)"
- />
- </div>
- </span>
- <span v-else-if="props.column.field === 'aliases_count'">
- {{ props.row.aliases.length }}
- </span>
- <span v-else-if="props.column.field === 'active'" class="flex items-center">
- <Toggle
- v-model="rows[props.row.originalIndex].active"
- @on="activateUsername(props.row.id)"
- @off="deactivateUsername(props.row.id)"
- />
- </span>
- <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
- <icon
- name="trash"
- class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
- @click.native="openDeleteModal(props.row.id)"
- />
- </span>
- </template>
- </vue-good-table>
- <div v-else class="bg-white rounded shadow overflow-x-auto">
- <div class="p-8 text-center text-lg text-grey-700">
- <h1 class="mb-6 text-xl text-indigo-800 font-semibold">
- This is where you can add and view additional usernames
- </h1>
- <div class="mx-auto mb-6 w-24 border-b-2 border-grey-200"></div>
- <p class="mb-4">
- When you add an additional username here you will be able to use it exactly like the
- username you signed up with!
- </p>
- <p class="mb-4">
- You can then separate aliases under your different usernames to reduce the chance of
- anyone linking ownership of them together. Great for compartmentalisation e.g. for work
- and personal emails.
- </p>
- <p>
- You can add a maximum of {{ usernameCount }} additional usernames. Deleted usernames still
- count towards your limit so please choose carefully.
- </p>
- </div>
- </div>
- <Modal :open="addUsernameModalOpen" @close="addUsernameModalOpen = false">
- <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
- <h2
- class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
- >
- Add new username
- </h2>
- <p class="mt-4 text-grey-700">
- Please choose additional usernames carefully as you can only add a maximum of
- {{ usernameCount }}. You cannot login with these usernames, only the one you originally
- signed up with.
- </p>
- <div class="mt-6">
- <p v-show="errors.newUsername" class="mb-3 text-red-500 text-sm">
- {{ errors.newUsername }}
- </p>
- <input
- v-model="newUsername"
- type="text"
- class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
- :class="errors.newUsername ? 'border-red-500' : ''"
- placeholder="johndoe"
- autofocus
- />
- <button
- @click="validateNewUsername"
- class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
- :class="addUsernameLoading ? 'cursor-not-allowed' : ''"
- :disabled="addUsernameLoading"
- >
- Add Username
- <loader v-if="addUsernameLoading" />
- </button>
- <button
- @click="addUsernameModalOpen = false"
- class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
- >
- Cancel
- </button>
- </div>
- </div>
- </Modal>
- <Modal :open="usernameDefaultRecipientModalOpen" @close="closeUsernameDefaultRecipientModal">
- <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
- <h2
- class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
- >
- Update Default Recipient
- </h2>
- <p class="my-4 text-grey-700">
- Select the default recipient for this username. This overrides the default recipient in
- your account settings. Leave it empty if you would like to use the default recipient in
- your account settings.
- </p>
- <multiselect
- v-model="defaultRecipient"
- :options="recipientOptions"
- :multiple="false"
- :close-on-select="true"
- :clear-on-select="false"
- :searchable="false"
- :allow-empty="true"
- placeholder="Select recipient"
- label="email"
- track-by="email"
- :preselect-first="false"
- :show-labels="false"
- >
- </multiselect>
- <div class="mt-6">
- <button
- type="button"
- @click="editDefaultRecipient()"
- class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus:outline-none"
- :class="editDefaultRecipientLoading ? 'cursor-not-allowed' : ''"
- :disabled="editDefaultRecipientLoading"
- >
- Update Default Recipient
- <loader v-if="editDefaultRecipientLoading" />
- </button>
- <button
- @click="closeUsernameDefaultRecipientModal()"
- class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
- >
- Cancel
- </button>
- </div>
- </div>
- </Modal>
- <Modal :open="deleteUsernameModalOpen" @close="closeDeleteModal">
- <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
- <h2
- class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
- >
- Delete username
- </h2>
- <p class="mt-4 text-grey-700">
- Are you sure you want to delete this username? You will no longer be able to receive any
- emails at this username subdomain. This will still count towards your additional username
- limit even once deleted.
- </p>
- <div class="mt-6">
- <button
- type="button"
- @click="deleteUsername(usernameIdToDelete)"
- class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
- :class="deleteUsernameLoading ? 'cursor-not-allowed' : ''"
- :disabled="deleteUsernameLoading"
- >
- Delete username
- <loader v-if="deleteUsernameLoading" />
- </button>
- <button
- @click="closeDeleteModal"
- class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
- >
- Cancel
- </button>
- </div>
- </div>
- </Modal>
- </div>
- </template>
- <script>
- import Modal from './../components/Modal.vue'
- import Toggle from './../components/Toggle.vue'
- import tippy from 'tippy.js'
- import Multiselect from 'vue-multiselect'
- export default {
- props: {
- initialUsernames: {
- type: Array,
- required: true,
- },
- usernameCount: {
- type: Number,
- required: true,
- },
- recipientOptions: {
- type: Array,
- required: true,
- },
- },
- components: {
- Modal,
- Toggle,
- Multiselect,
- },
- mounted() {
- this.addTooltips()
- },
- data() {
- return {
- newUsername: '',
- search: '',
- addUsernameLoading: false,
- addUsernameModalOpen: false,
- usernameIdToDelete: null,
- usernameIdToEdit: '',
- usernameDescriptionToEdit: '',
- deleteUsernameLoading: false,
- deleteUsernameModalOpen: false,
- usernameDefaultRecipientModalOpen: false,
- defaultRecipientUsernameToEdit: {},
- defaultRecipient: {},
- editDefaultRecipientLoading: false,
- errors: {},
- columns: [
- {
- label: 'Created',
- field: 'created_at',
- globalSearchDisabled: true,
- },
- {
- label: 'Username',
- field: 'username',
- },
- {
- label: 'Description',
- field: 'description',
- },
- {
- label: 'Default Recipient',
- field: 'default_recipient',
- sortable: false,
- globalSearchDisabled: true,
- },
- {
- label: 'Alias Count',
- field: 'aliases_count',
- type: 'number',
- globalSearchDisabled: true,
- },
- {
- label: 'Active',
- field: 'active',
- type: 'boolean',
- globalSearchDisabled: true,
- },
- {
- label: '',
- field: 'actions',
- sortable: false,
- globalSearchDisabled: true,
- },
- ],
- rows: this.initialUsernames,
- }
- },
- watch: {
- usernameIdToEdit: _.debounce(function() {
- this.addTooltips()
- }, 50),
- },
- methods: {
- addTooltips() {
- tippy('.tooltip', {
- arrow: true,
- arrowType: 'round',
- })
- },
- debounceToolips: _.debounce(function() {
- this.addTooltips()
- }, 50),
- validateNewUsername(e) {
- this.errors = {}
- if (!this.newUsername) {
- this.errors.newUsername = 'Username is required'
- } else if (!this.validUsername(this.newUsername)) {
- this.errors.newUsername = 'Username must only contain letters and numbers'
- } else if (this.newUsername.length > 20) {
- this.errors.newUsername = 'Username cannot be greater than 20 characters'
- }
- if (!this.errors.newUsername) {
- this.addNewUsername()
- }
- e.preventDefault()
- },
- addNewUsername() {
- this.addUsernameLoading = true
- axios
- .post(
- '/api/v1/usernames',
- JSON.stringify({
- username: this.newUsername,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(({ data }) => {
- this.addUsernameLoading = false
- this.rows.push(data.data)
- this.newUsername = ''
- this.addUsernameModalOpen = false
- this.success('Additional Username added')
- })
- .catch(error => {
- this.addUsernameLoading = false
- if (error.response.status === 403) {
- this.error('You have reached your additional username limit')
- } else if (error.response.status == 422) {
- this.error(error.response.data.errors.username[0])
- } else {
- this.error()
- }
- })
- },
- openDeleteModal(id) {
- this.deleteUsernameModalOpen = true
- this.usernameIdToDelete = id
- },
- closeDeleteModal() {
- this.deleteUsernameModalOpen = false
- this.usernameIdToDelete = null
- },
- openUsernameDefaultRecipientModal(username) {
- this.usernameDefaultRecipientModalOpen = true
- this.defaultRecipientUsernameToEdit = username
- this.defaultRecipient = username.default_recipient
- },
- closeUsernameDefaultRecipientModal() {
- this.usernameDefaultRecipientModalOpen = false
- this.defaultRecipientUsernameToEdit = {}
- this.defaultRecipient = {}
- },
- editUsername(username) {
- if (this.usernameDescriptionToEdit.length > 100) {
- return this.error('Description cannot be more than 100 characters')
- }
- axios
- .patch(
- `/api/v1/usernames/${username.id}`,
- JSON.stringify({
- description: this.usernameDescriptionToEdit,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(response => {
- username.description = this.usernameDescriptionToEdit
- this.usernameIdToEdit = ''
- this.usernameDescriptionToEdit = ''
- this.success('Username description updated')
- })
- .catch(error => {
- this.usernameIdToEdit = ''
- this.usernameDescriptionToEdit = ''
- this.error()
- })
- },
- editDefaultRecipient() {
- this.editDefaultRecipientLoading = true
- axios
- .patch(
- `/api/v1/usernames/${this.defaultRecipientUsernameToEdit.id}/default-recipient`,
- JSON.stringify({
- default_recipient: this.defaultRecipient ? this.defaultRecipient.id : '',
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(response => {
- let username = _.find(this.rows, ['id', this.defaultRecipientUsernameToEdit.id])
- username.default_recipient = this.defaultRecipient
- this.usernameDefaultRecipientModalOpen = false
- this.editDefaultRecipientLoading = false
- this.defaultRecipient = {}
- this.success("Additional Username's default recipient updated")
- })
- .catch(error => {
- this.usernameDefaultRecipientModalOpen = false
- this.editDefaultRecipientLoading = false
- this.defaultRecipient = {}
- this.error()
- })
- },
- activateUsername(id) {
- axios
- .post(
- `/api/v1/active-usernames`,
- JSON.stringify({
- id: id,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(response => {
- //
- })
- .catch(error => {
- this.error()
- })
- },
- deactivateUsername(id) {
- axios
- .delete(`/api/v1/active-usernames/${id}`)
- .then(response => {
- //
- })
- .catch(error => {
- this.error()
- })
- },
- deleteUsername(id) {
- this.deleteUsernameLoading = true
- axios
- .delete(`/api/v1/usernames/${id}`)
- .then(response => {
- this.rows = _.reject(this.rows, username => username.id === id)
- this.deleteUsernameModalOpen = false
- this.deleteUsernameLoading = false
- })
- .catch(error => {
- this.error()
- this.deleteUsernameLoading = false
- this.deleteUsernameModalOpen = false
- })
- },
- validUsername(username) {
- let re = /^[a-zA-Z0-9]*$/
- return re.test(username)
- },
- clipboardSuccess() {
- this.success('Copied to clipboard')
- },
- clipboardError() {
- this.error('Could not copy to clipboard')
- },
- success(text = '') {
- this.$notify({
- title: 'Success',
- text: text,
- type: 'success',
- })
- },
- error(text = 'An error has occurred, please try again later') {
- this.$notify({
- title: 'Error',
- text: text,
- type: 'error',
- })
- },
- },
- }
- </script>
|