Merge 1d85baa2f9
into 627ad302d2
This commit is contained in:
commit
c07d1cabc3
|
@ -1,5 +1,6 @@
|
|||
from .views import (
|
||||
index,
|
||||
dashboard_spa,
|
||||
pricing,
|
||||
setting,
|
||||
custom_alias,
|
||||
|
|
12
app/dashboard/views/dashboard_spa.py
Normal file
12
app/dashboard/views/dashboard_spa.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from flask import render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dashboard.base import dashboard_bp
|
||||
|
||||
|
||||
@dashboard_bp.route("/dashboard_spa", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def dashboard_spa():
|
||||
return render_template(
|
||||
"dashboard/dashboard_spa.html"
|
||||
)
|
538
static/js/dashboard-spa.js
Normal file
538
static/js/dashboard-spa.js
Normal file
|
@ -0,0 +1,538 @@
|
|||
// only allow lowercase letters, numbers, dots (.), dashes (-) and underscores (_)
|
||||
// don't allow dot at the start or end or consecutive dots
|
||||
const ALIAS_PREFIX_REGEX = /^(?!\.)(?!.*\.$)(?!.*\.\.)[0-9a-z-_.]+$/;
|
||||
|
||||
new Vue({
|
||||
el: '#dashboard-app',
|
||||
delimiters: ["[[", "]]"], // necessary to avoid conflict with jinja
|
||||
data: {
|
||||
showFilter: false,
|
||||
showStats: false,
|
||||
|
||||
mailboxes: [],
|
||||
|
||||
// variables for creating alias
|
||||
canCreateAlias: true,
|
||||
isLoading: true,
|
||||
aliasPrefixInput: "",
|
||||
aliasPrefixError: "",
|
||||
aliasSuffixes: [],
|
||||
aliasSelectedSignedSuffix: "",
|
||||
aliasNoteInput: "",
|
||||
defaultMailboxId: "",
|
||||
|
||||
// variables for aliases list
|
||||
isFetchingAlias: true,
|
||||
aliasesArray: [], // array of existing alias
|
||||
aliasesArrayOfNextPage: [], // to know there is a next page if not empty
|
||||
page: 0,
|
||||
isLoadingMoreAliases: false,
|
||||
searchString: "",
|
||||
filter: "", // TODO add more filters and also sorting when backend API is ready
|
||||
},
|
||||
computed: {
|
||||
isLastPage: function () {
|
||||
return this.aliasesArrayOfNextPage.length === 0;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
|
||||
if (store.get("showFilter")) {
|
||||
this.showFilter = true;
|
||||
}
|
||||
|
||||
if (store.get("showStats")) {
|
||||
this.showStats = true;
|
||||
}
|
||||
|
||||
await this.loadInitialData();
|
||||
|
||||
},
|
||||
methods: {
|
||||
// initialize mailboxes and alias options and aliases
|
||||
async loadInitialData() {
|
||||
this.isLoading = true;
|
||||
await this.loadMailboxes();
|
||||
await this.loadAliasOptions();
|
||||
this.isLoading = false;
|
||||
await this.loadAliases();
|
||||
},
|
||||
|
||||
async loadMailboxes() {
|
||||
try {
|
||||
const res = await fetch("/api/mailboxes");
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
this.mailboxes = result.mailboxes;
|
||||
this.defaultMailboxId = this.mailboxes.find((mailbox) => mailbox.default).id;
|
||||
} else {
|
||||
throw new Error("Could not load mailboxes");
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you try refreshing the page? ", "Could not load mailboxes");
|
||||
}
|
||||
},
|
||||
|
||||
async loadAliasOptions() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await fetch("/api/v5/alias/options");
|
||||
if (res.ok) {
|
||||
const aliasOptions = await res.json();
|
||||
this.aliasSuffixes = aliasOptions.suffixes;
|
||||
this.aliasSelectedSignedSuffix = this.aliasSuffixes[0].signed_suffix;
|
||||
this.canCreateAlias = aliasOptions.can_create;
|
||||
} else {
|
||||
throw new Error("Could not load alias options");
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you try refreshing the page? ", "Could not load alias options");
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
async loadAliases() {
|
||||
this.aliasesArray = [];
|
||||
this.page = 0;
|
||||
|
||||
this.aliasesArray = await this.fetchAlias(this.page, this.searchString);
|
||||
this.aliasesArrayOfNextPage = await this.fetchAlias(this.page + 1, this.searchString);
|
||||
|
||||
// use jquery multiple select plugin after Vue has rendered the aliases in the DOM
|
||||
this.$nextTick(() => {
|
||||
$('.mailbox-select').multipleSelect();
|
||||
$('.mailbox-select').removeClass('mailbox-select');
|
||||
});
|
||||
},
|
||||
|
||||
async fetchAlias(page, query) {
|
||||
this.isFetchingAlias = true;
|
||||
try {
|
||||
const res = await fetch(`/api/v2/aliases?page_id=${page}&${this.filter}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
...(query && { body: JSON.stringify({ query }) }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
this.isFetchingAlias = false;
|
||||
return result.aliases;
|
||||
} else {
|
||||
throw new Error("Aliases could not be loaded");
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you try refreshing the page? ", "Aliases could not be loaded");
|
||||
this.isFetchingAlias = false;
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async toggleFilter() {
|
||||
this.showFilter = !this.showFilter;
|
||||
store.set('showFilter', this.showFilter);
|
||||
},
|
||||
|
||||
async toggleStats() {
|
||||
this.showStats = !this.showStats;
|
||||
store.set('showStats', this.showStats);
|
||||
},
|
||||
|
||||
async toggleAlias(alias) {
|
||||
try {
|
||||
const res = await fetch(`/api/aliases/${alias.id}/toggle`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
alias.enabled = result.enabled;
|
||||
toastr.success(`${alias.email} is ${alias.enabled ? "enabled" : "disabled"}`);
|
||||
} else {
|
||||
throw new Error("Could not disable/enable alias");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
alias.enabled = !alias.enabled;
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Could not disable/enable alias");
|
||||
}
|
||||
},
|
||||
|
||||
async handleNoteChange(alias) {
|
||||
try {
|
||||
const res = await fetch(`/api/aliases/${alias.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
note: alias.note,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`Note saved for ${alias.email}`);
|
||||
} else {
|
||||
throw new Error("Note could not be saved");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Note could not be saved");
|
||||
}
|
||||
},
|
||||
|
||||
async handleDisplayNameChange(alias) {
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${alias.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: alias.name,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`Display name saved for ${alias.email}`);
|
||||
} else {
|
||||
throw new Error("Could not save Display name");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Could not save Display name")
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async handlePgpToggle(alias) {
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${alias.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
disable_pgp: alias.disable_pgp,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`PGP ${alias.disable_pgp ? "disabled" : "enabled"} for ${alias.email}`);
|
||||
} else {
|
||||
throw new Error("Could not toggle PGP")
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
alias.disable_pgp = !alias.disable_pgp;
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Could not toggle PGP");
|
||||
}
|
||||
},
|
||||
|
||||
async handlePin(alias) {
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${alias.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pinned: !alias.pinned,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alias.pinned = !alias.pinned;
|
||||
if (alias.pinned) {
|
||||
// make alias appear at the top of the alias list
|
||||
const index = this.aliasesArray.findIndex((a) => a.id === alias.id);
|
||||
this.aliasesArray.splice(index, 1);
|
||||
this.aliasesArray.unshift(alias);
|
||||
toastr.success(`${alias.email} is pinned`);
|
||||
} else {
|
||||
// unpin: make alias appear at the bottom of the alias list
|
||||
const index = this.aliasesArray.findIndex((a) => a.id === alias.id);
|
||||
this.aliasesArray.splice(index, 1);
|
||||
this.aliasesArray.push(alias);
|
||||
toastr.success(`${alias.email} is unpinned`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Alias could not be pinned");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Alias could not be pinned");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async handleDeleteAliasClick(alias, aliasDomainTrashUrl) {
|
||||
|
||||
let message = `If you don't want to receive emails from this alias, you can disable it. Please note that once deleted, it <b>can't</b> be restored.`;
|
||||
if (aliasDomainTrashUrl !== undefined) {
|
||||
message = `If you want to stop receiving emails from this alias, you can disable it instead. When it's deleted, it's moved to the domain
|
||||
<a href="${aliasDomainTrashUrl}">trash</a>`;
|
||||
}
|
||||
|
||||
const that = this;
|
||||
bootbox.dialog({
|
||||
title: `Delete alias ${alias.email}?`,
|
||||
message: message,
|
||||
onEscape: true,
|
||||
backdrop: true,
|
||||
centerVertical: true,
|
||||
buttons: {
|
||||
// show disable button only if alias is enabled
|
||||
...(alias.enabled ? {
|
||||
disable: {
|
||||
label: 'Disable it',
|
||||
className: 'btn-primary',
|
||||
callback: function () {
|
||||
that.disableAlias(alias);
|
||||
}
|
||||
}
|
||||
} : {}),
|
||||
|
||||
delete: {
|
||||
label: "Delete it, I don't need it anymore",
|
||||
className: 'btn-danger',
|
||||
callback: function () {
|
||||
that.deleteAlias(alias);
|
||||
}
|
||||
},
|
||||
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-outline-primary'
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async deleteAlias(alias) {
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${alias.id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`Alias ${alias.email} deleted`);
|
||||
this.aliasesArray = this.aliasesArray.filter((a) => a.id !== alias.id);
|
||||
} else {
|
||||
throw new Error("Alias could not be deleted");
|
||||
}
|
||||
|
||||
await this.loadAliasOptions();
|
||||
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Alias could not be deleted");
|
||||
}
|
||||
},
|
||||
|
||||
async disableAlias(alias) {
|
||||
try {
|
||||
if (alias.enabled === false) {
|
||||
return;
|
||||
}
|
||||
let res = await fetch(`/api/aliases/${alias.id}/toggle`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias_id: alias.id,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alias.enabled = false;
|
||||
toastr.success(`${alias.email} is disabled`);
|
||||
} else {
|
||||
throw new Error("Alias could not be disabled");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Alias could not be disabled");
|
||||
}
|
||||
},
|
||||
|
||||
// merge newAliases into currentAliases. If conflict, keep the new one
|
||||
mergeAliases(currentAliases, newAliases) {
|
||||
// dict of aliasId and alias to speed up research
|
||||
let newAliasesDict = {};
|
||||
for (let i = 0; i < newAliases.length; i++) {
|
||||
let alias = newAliases[i];
|
||||
newAliasesDict[alias.id] = alias;
|
||||
}
|
||||
|
||||
let ret = [];
|
||||
|
||||
// keep track of added aliases
|
||||
let alreadyAddedId = {};
|
||||
for (let i = 0; i < currentAliases.length; i++) {
|
||||
let alias = currentAliases[i];
|
||||
if (newAliasesDict[alias.id]) ret.push(newAliasesDict[alias.id]);
|
||||
else ret.push(alias);
|
||||
|
||||
alreadyAddedId[alias.id] = true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < newAliases.length; i++) {
|
||||
let alias = newAliases[i];
|
||||
if (!alreadyAddedId[alias.id]) {
|
||||
ret.push(alias);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
async loadMoreAliases() {
|
||||
this.isLoadingMoreAliases = true;
|
||||
this.page++;
|
||||
|
||||
// we already fetched aliases of the next page, just merge it
|
||||
this.aliasesArray = this.mergeAliases(this.aliasesArray, this.aliasesArrayOfNextPage);
|
||||
|
||||
// fetch next page in advance to know if there is a next page
|
||||
this.aliasesArrayOfNextPage = await this.fetchAlias(this.page + 1, this.searchString);
|
||||
|
||||
// use jquery multiple select plugin after Vue has rendered the aliases in the DOM
|
||||
this.$nextTick(() => {
|
||||
$('.mailbox-select').multipleSelect();
|
||||
$('.mailbox-select').removeClass('mailbox-select');
|
||||
});
|
||||
|
||||
this.isLoadingMoreAliases = false;
|
||||
},
|
||||
|
||||
resetFilter() {
|
||||
this.searchString = "";
|
||||
this.filter = "";
|
||||
this.loadAliases();
|
||||
},
|
||||
|
||||
// enable or disable the 'Create' button depending on whether the alias prefix is valid or not
|
||||
handleAliasPrefixInput() {
|
||||
this.aliasPrefixInput = this.aliasPrefixInput.toLowerCase();
|
||||
if (this.aliasPrefixInput.match(ALIAS_PREFIX_REGEX)) {
|
||||
document.querySelector('.bootbox-accept').classList.remove('disabled');
|
||||
this.aliasPrefixError = "";
|
||||
} else {
|
||||
document.querySelector('.bootbox-accept').classList.add('disabled');
|
||||
this.aliasPrefixError = this.aliasPrefixInput.length > 0 ? "Only lowercase letters, numbers, dots (.), dashes (-) and underscores (_) are supported." : "";
|
||||
}
|
||||
},
|
||||
|
||||
async createCustomAlias() {
|
||||
try {
|
||||
const res = await fetch("/api/v3/alias/custom/new", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
alias_prefix: this.aliasPrefixInput,
|
||||
signed_suffix: this.aliasSelectedSignedSuffix,
|
||||
mailbox_ids: [this.defaultMailboxId],
|
||||
note: this.aliasNoteInput,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const alias = await res.json();
|
||||
this.aliasesArray.unshift(alias);
|
||||
toastr.success(`Alias ${alias.email} created`);
|
||||
|
||||
// use jquery multiple select plugin after Vue has rendered the aliases in the DOM
|
||||
this.$nextTick(() => {
|
||||
$('.mailbox-select').multipleSelect();
|
||||
$('.mailbox-select').removeClass('mailbox-select');
|
||||
});
|
||||
|
||||
} else {
|
||||
const error = await res.json();
|
||||
toastr.error(error.error, "Alias could not be created");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Alias could not be created");
|
||||
}
|
||||
|
||||
this.aliasPrefixInput = "";
|
||||
this.aliasNoteInput = "";
|
||||
await this.loadAliasOptions();
|
||||
},
|
||||
|
||||
handleNewCustomAliasClick() {
|
||||
const that = this;
|
||||
bootbox.dialog({
|
||||
title: "Create an alias",
|
||||
message: this.$refs.createAliasModal,
|
||||
size: 'large',
|
||||
onEscape: true,
|
||||
backdrop: true,
|
||||
centerVertical: true,
|
||||
onShown: function (e) {
|
||||
document.getElementById('create-alias-prefix-input').focus();
|
||||
if (that.aliasPrefixInput) {
|
||||
that.handleAliasPrefixInput();
|
||||
}
|
||||
},
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-outline-primary'
|
||||
},
|
||||
confirm: {
|
||||
label: 'Create',
|
||||
className: 'btn-primary disabled',
|
||||
callback: function () {
|
||||
that.createCustomAlias();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
async function handleMailboxChange(event) {
|
||||
aliasId = event.target.dataset.aliasId;
|
||||
aliasEmail = event.target.dataset.aliasEmail;
|
||||
const selectedOptions = event.target.selectedOptions;
|
||||
const mailbox_ids = Array.from(selectedOptions).map((selectedOption) => selectedOption.value);
|
||||
|
||||
if (mailbox_ids.length === 0) {
|
||||
toastr.error("You must select at least a mailbox", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${aliasId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mailbox_ids: mailbox_ids,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`Mailbox updated for ${aliasEmail}`);
|
||||
} else {
|
||||
throw new Error("Mailbox could not be updated");
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Mailbox could not be updated");
|
||||
}
|
||||
|
||||
}
|
318
templates/dashboard/dashboard_spa.html
Normal file
318
templates/dashboard/dashboard_spa.html
Normal file
|
@ -0,0 +1,318 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% set active_page = "dashboard" %}
|
||||
{% block head %}
|
||||
|
||||
<style>
|
||||
.alias-activity {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-group-border-left {
|
||||
border-left: 1px #fbfbfb4f solid;
|
||||
}
|
||||
|
||||
em {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
{# CSS to change collapse button display from https://stackoverflow.com/a/31967516/1428034 #}
|
||||
|
||||
[data-toggle="collapse"].collapsed .if-not-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-toggle="collapse"]:not(.collapsed) .if-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alias-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Aliases{% endblock %}
|
||||
{% block default_content %}
|
||||
|
||||
<div id="dashboard-app">
|
||||
<!-- Controls: buttons & search -->
|
||||
<div>
|
||||
<div class="row mb-3">
|
||||
<div class="col d-flex flex-wrap justify-content-between">
|
||||
<div class="mb-1">
|
||||
<div class="btn-group" role="group">
|
||||
<button @click="handleNewCustomAliasClick"
|
||||
class="btn btn-primary mr-2"
|
||||
:disabled="isLoading || !canCreateAlias">
|
||||
<i class="fa fa-plus"></i> New custom alias
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group">
|
||||
<a @click="toggleStats()" class="btn btn-outline-secondary">
|
||||
<span v-if="!showStats">
|
||||
<i class="fe fe-chevrons-down"></i>
|
||||
Show stats
|
||||
</span>
|
||||
<span v-else>
|
||||
<i class="fe fe-chevrons-up"></i>
|
||||
Hide stats
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a @click="toggleFilter()" class="btn btn-outline-secondary">
|
||||
<span v-if="!showFilter">
|
||||
<i class="fe fe-chevrons-down"></i>
|
||||
Show filters
|
||||
</span>
|
||||
<span v-else>
|
||||
<i class="fe fe-chevrons-up"></i>
|
||||
Hide filters
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO Global Stats -->
|
||||
<div class="row" v-if="showStats"></div>
|
||||
<!-- END Global Stats -->
|
||||
<div class="row mb-2" v-if="showFilter" id="filter-control">
|
||||
<!-- Filter Control -->
|
||||
<div class="col d-flex flex-wrap">
|
||||
<div class="flex-grow-0 mr-2">
|
||||
<select name="filter"
|
||||
v-model="filter"
|
||||
@change="loadAliases"
|
||||
class="form-control mr-3 shadow flex-grow-0">
|
||||
<option value="">
|
||||
All aliases
|
||||
</option>
|
||||
<option value="pinned">
|
||||
Pinned aliases
|
||||
</option>
|
||||
<option value="enabled">
|
||||
Enabled aliases
|
||||
</option>
|
||||
<option value="disabled">
|
||||
Disabled aliases
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Search -->
|
||||
<div class="flex-grow-1">
|
||||
<input type="search"
|
||||
name="query"
|
||||
v-model="searchString"
|
||||
placeholder="Enter to search for alias"
|
||||
class="form-control shadow mr-2"
|
||||
@keyup.enter="loadAliases">
|
||||
</div>
|
||||
<div v-if="searchString || filter" class="ml-2">
|
||||
<a @click="resetFilter()" class="btn btn-outline-secondary">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END Controls: buttons & search -->
|
||||
<!-- Alias list -->
|
||||
<div id="alias-list" class="row">
|
||||
<div v-for="(alias, index) in aliasesArray"
|
||||
:key="alias.id"
|
||||
class="col-12 col-md-6">
|
||||
<!-- TODO highlight newly created alias -->
|
||||
<div class="card p-4 shadow-sm"
|
||||
:class="{ 'alias-disabled': !alias.enabled }">
|
||||
<div class="d-flex justify-space-between">
|
||||
<div class="d-flex">
|
||||
<h5 class="clipboard mb-0 text-break d-inline-block cursor"
|
||||
:data-toggle="alias.enabled ? 'tooltip' : null"
|
||||
title="Click to copy"
|
||||
:data-clipboard-text="alias.enabled ? alias.email : null">
|
||||
[[ alias.email ]]
|
||||
</h5>
|
||||
<span v-if="alias.pinned"
|
||||
data-toggle="tooltip"
|
||||
title="Click to unpin"
|
||||
@click="handlePin(alias)"
|
||||
class="cursor ml-2">
|
||||
<i class="fa fa-thumb-tack"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<label class="custom-switch cursor"
|
||||
data-toggle="tooltip"
|
||||
:title="alias.enabled ? 'Disable alias - Stop receiving emails sent to this alias' : 'Enable alias - Start receiving emails sent to this alias'">
|
||||
<input type="checkbox"
|
||||
class="enable-disable-alias custom-switch-input"
|
||||
v-model="alias.enabled"
|
||||
@change="toggleAlias(alias)">
|
||||
<span class="custom-switch-indicator"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Email Activity -->
|
||||
<div v-if="!alias.enabled" class="small-text mb-4">
|
||||
Alias is disabled, you will not receive any email from this alias.
|
||||
</div>
|
||||
<div v-else-if="alias.latest_activity" class="small-text mb-4">
|
||||
[[ alias.latest_activity.contact.email ]]
|
||||
<span v-if="alias.latest_activity.action === 'bounched'">
|
||||
<i class="fa fa-exclamation-triangle mr-2"></i> Cannot be forwarded to your mailbox.
|
||||
</span>
|
||||
<span v-if="alias.latest_activity.action === 'block'">
|
||||
<i class="fa fa-ban mr-2"></i> Blocked.
|
||||
</span>
|
||||
<!-- TODO improve date formatting -->
|
||||
Created [[ alias.creation_date ]].
|
||||
</div>
|
||||
<div v-else class="small-text mb-4">
|
||||
No emails received/sent in the last 14 days.
|
||||
Created [[ alias.creation_date ]].
|
||||
</div>
|
||||
<!-- END Email Activity -->
|
||||
<div class="small-text mb-4">
|
||||
<label class="mb-0" :for="`note-${alias.id}`">Note</label>
|
||||
<textarea v-model="alias.note" name="note" class="form-control" rows="2" placeholder="e.g. where the alias is used or why is it created" @change="handleNoteChange(alias)">[[ alias.note ]]</textarea>
|
||||
</div>
|
||||
<div v-if="mailboxes && mailboxes.length > 1" class="mb-4">
|
||||
<label class="small-text mb-0" :for="`mailbox-${alias.id}`">Current mailbox</label>
|
||||
<div class="d-flex">
|
||||
<!-- not using Vue's event @change below for compatibility with jquery multipleSelect plugin -->
|
||||
<select required
|
||||
:id="`mailbox-${alias.id}`"
|
||||
class="mailbox-select flex-grow-1"
|
||||
name="mailbox"
|
||||
:data-alias-id="alias.id"
|
||||
:data-alias-email="alias.email"
|
||||
onchange="handleMailboxChange(event)"
|
||||
multiple>
|
||||
<option v-for="mailbox in mailboxes"
|
||||
:value="mailbox.id"
|
||||
:key="mailbox.id"
|
||||
:selected="alias.mailboxes.find(m => m.id === mailbox.id)">
|
||||
[[ mailbox.email ]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- TODO mailbox owned by another user -->
|
||||
</div>
|
||||
<div class="small-text mb-4">
|
||||
<label class="mb-0" :for="`alias-name-${alias.id}`">Display name</label>
|
||||
<input :id="`alias-name-${alias.id}`"
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="display-name"
|
||||
placeholder="e.g. John Doe"
|
||||
v-model="alias.name"
|
||||
@change="handleDisplayNameChange(alias)">
|
||||
<div>
|
||||
'From: [[ alias.name ]] <[[ alias.email ]]>' will be in the email header when you send an email from this alias.
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="alias.support_pgp" class="d-flex mb-2">
|
||||
<label class="custom-switch cursor pl-0">
|
||||
<input type="checkbox"
|
||||
class="custom-switch-input"
|
||||
v-model="alias.disable_pgp"
|
||||
:true-value="false"
|
||||
:false-value="true"
|
||||
@change="handlePgpToggle(alias)">
|
||||
<span class="custom-switch-indicator"></span>
|
||||
</label>
|
||||
<label for="`enable-disable-pgp-${alias.id}`" class="ml-2 mb-0">PGP</label>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<button class="btn btn-sm"
|
||||
:class="[alias.pinned ? 'btn-primary' : 'btn-outline-primary']"
|
||||
@click="handlePin(alias)">
|
||||
<i class="fa fa-thumb-tack"></i>
|
||||
<span v-if="alias.pinned">Pinned</span>
|
||||
<span v-else>Pin</span>
|
||||
</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<button class="btn btn-link btn-sm">
|
||||
<i class="fe fe-share-2"></i>
|
||||
Transfer
|
||||
</button>
|
||||
<!-- TODO aliasDomainTrashUrl -->
|
||||
<button class="btn btn-link text-danger btn-sm ml-1"
|
||||
@click="handleDeleteAliasClick(alias)">
|
||||
<i class="fa fa-trash"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END Alias list -->
|
||||
<div v-if="isFetchingAlias" class="row">
|
||||
<div class="col-12 text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isLastPage && !isFetchingAlias" class="row">
|
||||
<!-- load more aliases button -->
|
||||
<div class="col-12 col-md-6 mb-2">
|
||||
<button class="btn btn-outline-primary btn-block"
|
||||
@click="loadMoreAliases"
|
||||
:disabled="isLoadingMoreAliases">
|
||||
<span class="fe fe-chevron-down"></span>
|
||||
Load more aliases
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- HTML for create alias modal -->
|
||||
<div style="display: none">
|
||||
<div ref="createAliasModal">
|
||||
<label class="mb-0" for="create-alias-prefix-input">Alias email</label>
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6 pr-sm-1" style="min-width: 4em">
|
||||
<input v-model="aliasPrefixInput"
|
||||
@input="handleAliasPrefixInput"
|
||||
type="text"
|
||||
id="create-alias-prefix-input"
|
||||
name="alias-prefix"
|
||||
autofocus
|
||||
class="form-control"
|
||||
placeholder="newsletter-123_xyz"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-sm-6 pl-sm-1">
|
||||
<select class="form-control"
|
||||
v-model="aliasSelectedSignedSuffix"
|
||||
name="alias-suffix"
|
||||
required>
|
||||
<option v-for="suffix in aliasSuffixes"
|
||||
:key="suffix.signed_suffix"
|
||||
:value="suffix.signed_suffix">
|
||||
[[ suffix.suffix ]]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-danger"
|
||||
style="font-size: 12px"
|
||||
v-if="aliasPrefixError">
|
||||
<div class="col">[[ aliasPrefixError ]]</div>
|
||||
</div>
|
||||
<label class="w-100">
|
||||
Note (optional)
|
||||
<textarea v-model="aliasNoteInput" name="note" class="form-control" rows="2" placeholder="e.g. where the alias is used or why is it created"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
|
||||
<script src="/static/js/dashboard-spa.js"></script>
|
||||
<script>// TODO show intro tutorial with introJs()</script>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue