Polish UI elements and fix styling issues.
- Change global font to Inter. - Introduce global top nav bar. - Restyle form inputs to have inline labels. - Restyle form inputs to have inline lengt counters. - Override glitchy Buefy animations (sidebar, toast etc.) - Fix tag alignment inside tables in responsive view. - Refactor import page UI. - Miscellaneous styling fixes. - Add missing Fontello icons.
This commit is contained in:
parent
942eb7c3d8
commit
e2e65b1bc0
|
@ -9,9 +9,10 @@ listmonk is a standalone, self-hosted, newsletter and mailing list manager. It i
|
|||
### Installation and use
|
||||
|
||||
- Download the [latest release](https://github.com/knadh/listmonk/releases) for your platform and extract the listmonk binary. For example: `tar -C $HOME/listmonk -xzf listmonk_$VERSION_$OS_$ARCH.tar.gz`
|
||||
- Navigate to the directory containing the binary (`cd $HOME/listmonk`) and run `./listmonk --new-config` to generate a sample `config.toml` and add your configuration (SMTP and Postgres DB credentials primarily).
|
||||
- Navigate to the directory containing the binary (`cd $HOME/listmonk`) and run `./listmonk --new-config` to generate a sample `config.toml` and add the DB configuration.
|
||||
- `./listmonk --install` to setup the DB.
|
||||
- Run `./listmonk` and visit `http://localhost:9000`.
|
||||
- Visit the `Settings` page to configure your instance.
|
||||
- Since there is no user auth yet, it's best to put listmonk behind a proxy like Nginx and setup basicauth on all endpoints except for the few endpoints that need to be public. Here is a [sample nginx config](https://github.com/knadh/listmonk/wiki/Production-Nginx-config) for production use.
|
||||
|
||||
### Configuration and customization
|
||||
|
|
|
@ -440,6 +440,48 @@
|
|||
"content-save-outline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "80491c76df0c066833e0f8211903d37c",
|
||||
"css": "minus",
|
||||
"code": 59423,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M791 541H209V459H791V541Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"minus"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "a7a02467d65aabd7cd61903ea3e855b6",
|
||||
"css": "arrow-up",
|
||||
"code": 59424,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M541 834H459V334L228.5 562.5 169.9 503.9 500 173.8 830.1 503.9 771.5 562.5 541 334V834Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"arrow-up"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "a9b97a98d1427ca1c4f90b2f8f4f03c1",
|
||||
"css": "arrow-down",
|
||||
"code": 59425,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M459 166H541V666L771.5 437.5 830.1 496.1 500 826.2 169.9 496.1 228.5 437.5 459 666V166Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"arrow-down"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "f4ad3f6d071a0bfb3a8452b514ed0892",
|
||||
"css": "vector-square",
|
||||
|
@ -1364,20 +1406,6 @@
|
|||
"arrow-collapse-all"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "a9b97a98d1427ca1c4f90b2f8f4f03c1",
|
||||
"css": "arrow-down",
|
||||
"code": 983109,
|
||||
"src": "custom_icons",
|
||||
"selected": false,
|
||||
"svg": {
|
||||
"path": "M459 166H541V666L771.5 437.5 830.1 496.1 500 826.2 169.9 496.1 228.5 437.5 459 666V166Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"arrow-down"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "578692c5a0b505985bf797ee8ebce545",
|
||||
"css": "arrow-down-thick",
|
||||
|
@ -1686,20 +1714,6 @@
|
|||
"arrow-top-left"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "a7a02467d65aabd7cd61903ea3e855b6",
|
||||
"css": "arrow-up",
|
||||
"code": 983133,
|
||||
"src": "custom_icons",
|
||||
"selected": false,
|
||||
"svg": {
|
||||
"path": "M541 834H459V334L228.5 562.5 169.9 503.9 500 173.8 830.1 503.9 771.5 562.5 541 334V834Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"arrow-up"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "00e74cb9bfa86a1b90b39d2d8132c3b1",
|
||||
"css": "arrow-up-thick",
|
||||
|
@ -12774,20 +12788,6 @@
|
|||
"minecraft"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "80491c76df0c066833e0f8211903d37c",
|
||||
"css": "minus",
|
||||
"code": 983924,
|
||||
"src": "custom_icons",
|
||||
"selected": false,
|
||||
"svg": {
|
||||
"path": "M791 541H209V459H791V541Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"minus"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "4dae8d34e12ee29474c244f25a6cbc1c",
|
||||
"css": "minus-box",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
|
||||
<link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,600" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<script src="<%= BASE_URL %>api/config.js"></script>
|
||||
</head>
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<b-navbar :fixed-top="true">
|
||||
<template slot="brand">
|
||||
<div class="logo">
|
||||
<router-link :to="{name: 'dashboard'}">
|
||||
<img class="full" src="@/assets/logo.svg"/>
|
||||
<img class="favicon" src="@/assets/favicon.png"/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="end">
|
||||
<b-navbar-item tag="div"></b-navbar-item>
|
||||
</template>
|
||||
</b-navbar>
|
||||
|
||||
<div class="wrapper">
|
||||
<section class="sidebar">
|
||||
<b-sidebar
|
||||
type="is-white"
|
||||
position="static"
|
||||
mobile="reduce"
|
||||
:fullheight="true"
|
||||
|
@ -10,11 +24,6 @@
|
|||
:can-cancel="false"
|
||||
>
|
||||
<div>
|
||||
<div class="logo">
|
||||
<a href="/"><img class="full" src="@/assets/logo.svg"/></a>
|
||||
<img class="favicon" src="@/assets/favicon.png"/>
|
||||
<p class="is-size-7 has-text-grey version">{{ version }}</p>
|
||||
</div>
|
||||
<b-menu :accordion="false">
|
||||
<b-menu-list>
|
||||
<b-menu-item :to="{name: 'dashboard'}" tag="router-link"
|
||||
|
@ -89,6 +98,7 @@
|
|||
|
||||
<router-view :key="$route.fullPath" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-loading v-if="!isLoaded" active>
|
||||
<div class="has-text-centered">
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@import "~bulma/sass/components/menu";
|
||||
@import "~bulma/sass/components/message";
|
||||
@import "~bulma/sass/components/modal";
|
||||
@import "~bulma/sass/components/navbar";
|
||||
@import "~bulma/sass/components/pagination";
|
||||
@import "~bulma/sass/components/tabs";
|
||||
@import "~bulma/sass/form/_all";
|
||||
|
|
|
@ -71,3 +71,6 @@
|
|||
.mdi-chevron-right:before { content: '\e81c'; } /* '' */
|
||||
.mdi-chevron-left:before { content: '\e81d'; } /* '' */
|
||||
.mdi-content-save-outline:before { content: '\e81e'; } /* '' */
|
||||
.mdi-minus:before { content: '\e81f'; } /* '' */
|
||||
.mdi-arrow-up:before { content: '\e820'; } /* '' */
|
||||
.mdi-arrow-down:before { content: '\e821'; } /* '' */
|
||||
|
|
Binary file not shown.
|
@ -1,14 +1,16 @@
|
|||
/* Import Bulma to set variables */
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
|
||||
$body-family: "IBM Plex Sans", "Helvetica Neue", sans-serif;
|
||||
$body-family: "Inter", "Helvetica Neue", sans-serif;
|
||||
$body-size: 15px;
|
||||
$background: $white-bis;
|
||||
$body-background-color: $white-bis;
|
||||
$primary: #7f2aff;
|
||||
$green: #4caf50;
|
||||
$turquoise: $green;
|
||||
$red: #ff5722;
|
||||
$link: $primary;
|
||||
$input-placeholder-color: $black-ter;
|
||||
$input-placeholder-color: $grey-light;
|
||||
|
||||
$colors: map-merge($colors, (
|
||||
"turquoise": ($green, $green-invert),
|
||||
|
@ -77,35 +79,58 @@ section {
|
|||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
box-shadow: 0 0 2px $grey-lighter;
|
||||
}
|
||||
|
||||
/* Two column sidebar+body layout */
|
||||
#app {
|
||||
min-height: 100%;
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
> .sidebar {
|
||||
.sidebar {
|
||||
flex-shrink: 1;
|
||||
box-shadow: 0 0 5px #eee;
|
||||
border-right: 1px solid #eee;
|
||||
box-shadow: 0 0 3px $grey-lighter;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.main {
|
||||
background: $white;
|
||||
margin-left: 15px;
|
||||
padding: 30px;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
box-shadow: 0 0 3px $grey-lighter;
|
||||
}
|
||||
.navbar-brand {
|
||||
padding: 0 0 0 25px;
|
||||
.favicon {
|
||||
display: none;
|
||||
}
|
||||
.full {
|
||||
max-height: 20px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.favicon {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.b-sidebar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
> .main {
|
||||
margin: 30px 30px 30px 45px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
top: 75px;
|
||||
|
||||
.b-sidebar {
|
||||
.logo {
|
||||
padding: 15px;
|
||||
}
|
||||
.sidebar-content {
|
||||
border-right: 1px solid #eee;
|
||||
background: transparent;
|
||||
}
|
||||
.menu-list {
|
||||
.router-link-exact-active {
|
||||
|
@ -116,14 +141,14 @@ section {
|
|||
margin-right: 0;
|
||||
}
|
||||
> li {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
a {
|
||||
padding-left: 25px;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
margin-bottom: 30px;
|
||||
a {
|
||||
border-radius: 0;
|
||||
}
|
||||
.favicon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,10 +206,6 @@ section {
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* Toasts */
|
||||
.notices .toast {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Fix for button primary colour. */
|
||||
.button.is-primary {
|
||||
|
@ -198,11 +219,39 @@ section {
|
|||
}
|
||||
|
||||
.autocomplete .dropdown-content {
|
||||
background-color: $white-bis;
|
||||
background-color: $white-ter;
|
||||
}
|
||||
|
||||
.input, .taginput .taginput-container.is-focusable, .textarea {
|
||||
box-shadow: inset 2px 2px 0px $white-ter;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
.field {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.control {
|
||||
position: relative;
|
||||
|
||||
.help.counter {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
color: $grey;
|
||||
}
|
||||
|
||||
.help {
|
||||
color: $grey;
|
||||
color: $grey-light;
|
||||
}
|
||||
}
|
||||
.has-numberinput .field, .field.is-grouped {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
|
@ -267,6 +316,10 @@ section.dashboard {
|
|||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.counts .column {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.level-item {
|
||||
background-color: $white-bis;
|
||||
padding: 30px;
|
||||
|
@ -296,6 +349,11 @@ section.lists {
|
|||
}
|
||||
}
|
||||
|
||||
/* List selector */
|
||||
.list-tags {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Subscribers page */
|
||||
.subscribers-controls {
|
||||
padding-bottom: 15px;
|
||||
|
@ -520,6 +578,40 @@ section.campaign {
|
|||
}
|
||||
}
|
||||
|
||||
/* Vue animations */
|
||||
.slide-enter-active, .slide-leave-active {
|
||||
transition: opacity 50ms;
|
||||
max-height: none;
|
||||
}
|
||||
.slide-enter, .slide-leave-to {
|
||||
transition: opacity 50ms;
|
||||
opacity: 0;
|
||||
max-height: none;
|
||||
}
|
||||
.slide-leave-active, .slide-leave-to {
|
||||
transition: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Toasts */
|
||||
.notices {
|
||||
@keyframes scale {
|
||||
0% {
|
||||
scale: 1;
|
||||
}
|
||||
50% {
|
||||
scale: 1.3;
|
||||
}
|
||||
100% {
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
.toast {
|
||||
animation: scale 300ms ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1450px) and (min-width: 769px) {
|
||||
section.campaigns {
|
||||
/* Fold the stats labels until the card view */
|
||||
|
@ -539,9 +631,33 @@ section.campaign {
|
|||
}
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
html, body {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#app .main {
|
||||
margin-left: 5px;
|
||||
padding: 30px 20px 30px 20px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
.full {
|
||||
display: none;
|
||||
}
|
||||
.favicon {
|
||||
display: block;
|
||||
}
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.b-sidebar {
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
/* Hide sidebar menu captions on mobile */
|
||||
.b-sidebar .sidebar-content.is-mini-mobile {
|
||||
.menu-list li {
|
||||
.menu-list {
|
||||
li {
|
||||
margin-bottom: 30px;
|
||||
|
||||
span:nth-child(2) {
|
||||
|
@ -551,22 +667,21 @@ section.campaign {
|
|||
scale: 1.4;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
text-align: center;
|
||||
.full {
|
||||
display: none;
|
||||
> li {
|
||||
a {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.favicon {
|
||||
display: block;
|
||||
}
|
||||
.version {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#app > .content {
|
||||
margin: 15px;
|
||||
td .tags {
|
||||
display: block;
|
||||
text-align: right;
|
||||
|
||||
.tag:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div class="field">
|
||||
<b-field :label="label + (selectedItems ? ` (${selectedItems.length})` : '')">
|
||||
<div :class="classes">
|
||||
<div :class="['list-tags', ...classes]">
|
||||
<b-taglist>
|
||||
<b-tag v-for="l in selectedItems"
|
||||
:key="l.id"
|
||||
|
@ -13,9 +12,10 @@
|
|||
</b-tag>
|
||||
</b-taglist>
|
||||
</div>
|
||||
</b-field>
|
||||
|
||||
<b-field :message="message">
|
||||
<b-field :message="message"
|
||||
:label="label + (selectedItems ? ` (${selectedItems.length})` : '')"
|
||||
label-position="on-border">
|
||||
<b-autocomplete
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
|
|
|
@ -63,7 +63,7 @@ export default class utils {
|
|||
// UI shortcuts.
|
||||
static confirm = (msg, onConfirm, onCancel) => {
|
||||
Dialog.confirm({
|
||||
scroll: 'keep',
|
||||
scroll: 'clip',
|
||||
message: !msg ? 'Are you sure?' : msg,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
|
@ -72,7 +72,7 @@ export default class utils {
|
|||
|
||||
static prompt = (msg, inputAttrs, onConfirm, onCancel) => {
|
||||
Dialog.prompt({
|
||||
scroll: 'keep',
|
||||
scroll: 'clip',
|
||||
message: msg,
|
||||
confirmText: 'OK',
|
||||
inputAttrs: {
|
||||
|
@ -91,7 +91,7 @@ export default class utils {
|
|||
message: msg,
|
||||
type: !typ ? 'is-success' : typ,
|
||||
queue: false,
|
||||
duration: duration || 3000,
|
||||
duration: duration || 2000,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -33,22 +33,22 @@
|
|||
<b-loading :active="loading.campaigns"></b-loading>
|
||||
|
||||
<b-tabs type="is-boxed" :animated="false" v-model="activeTab">
|
||||
<b-tab-item label="Campaign" icon="rocket-launch-outline">
|
||||
<b-tab-item label="Campaign" label-position="on-border" icon="rocket-launch-outline">
|
||||
<section class="wrap">
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<form @submit.prevent="onSubmit">
|
||||
<b-field label="Name">
|
||||
<b-field label="Name" label-position="on-border">
|
||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" :disabled="!canEdit"
|
||||
placeholder="Name" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Subject">
|
||||
<b-field label="Subject" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.subject" :disabled="!canEdit"
|
||||
placeholder="Subject" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="From address">
|
||||
<b-field label="From address" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.fromEmail" :disabled="!canEdit"
|
||||
placeholder="Your Name <noreply@yoursite.com>" required></b-input>
|
||||
</b-field>
|
||||
|
@ -62,24 +62,28 @@
|
|||
placeholder="Lists to send to"
|
||||
></list-selector>
|
||||
|
||||
<b-field label="Template">
|
||||
<b-field label="Template" label-position="on-border">
|
||||
<b-select placeholder="Template" v-model="form.templateId"
|
||||
:disabled="!canEdit" required>
|
||||
<option v-for="t in templates" :value="t.id" :key="t.id">{{ t.name }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Tags">
|
||||
<b-field label="Tags" label-position="on-border">
|
||||
<b-taginput v-model="form.tags" :disabled="!canEdit"
|
||||
ellipsis icon="tag-outline" placeholder="Tags"></b-taginput>
|
||||
</b-field>
|
||||
<hr />
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-2">
|
||||
<b-field label="Send later?">
|
||||
<b-switch v-model="form.sendLater" :disabled="!canEdit"></b-switch>
|
||||
</b-field>
|
||||
|
||||
<b-field v-if="form.sendLater" label="Send at">
|
||||
</div>
|
||||
<div class="column">
|
||||
<br />
|
||||
<b-field v-if="form.sendLater">
|
||||
<b-datetimepicker
|
||||
v-model="form.sendAtDate"
|
||||
:disabled="!canEdit"
|
||||
|
@ -90,6 +94,8 @@
|
|||
horizontal-time-picker>
|
||||
</b-datetimepicker>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<b-field v-if="isNew">
|
||||
|
@ -267,11 +273,7 @@ export default Vue.extend({
|
|||
return new Promise((resolve) => {
|
||||
this.$api.updateCampaign(this.data.id, data).then((d) => {
|
||||
this.data = d;
|
||||
this.$buefy.toast.open({
|
||||
message: `'${d.name}' ${typMsg}`,
|
||||
type: 'is-success',
|
||||
queue: false,
|
||||
});
|
||||
this.$utils.toast(`'${d.name}' ${typMsg}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
@ -327,11 +329,7 @@ export default Vue.extend({
|
|||
} else {
|
||||
const intID = parseInt(id, 10);
|
||||
if (intID <= 0 || Number.isNaN(intID)) {
|
||||
this.$buefy.toast.open({
|
||||
message: 'Invalid campaign',
|
||||
type: 'is-danger',
|
||||
queue: false,
|
||||
});
|
||||
this.$utils.toast('Invalid campaign');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
</b-table-column>
|
||||
|
||||
<b-table-column class="actions" width="13%" align="right">
|
||||
<div>
|
||||
<a href="" v-if="canStart(props.row)"
|
||||
@click.prevent="$utils.confirm(null,
|
||||
() => changeCampaignStatus(props.row, 'running'))">
|
||||
|
@ -169,6 +170,7 @@
|
|||
() => deleteCampaign(props.row))">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</a>
|
||||
</div>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="empty" v-if="!loading.campaigns">
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
<section v-if="isFree()" class="wrap-small">
|
||||
<form @submit.prevent="onSubmit" class="box">
|
||||
<div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field label="Mode">
|
||||
<div>
|
||||
<b-radio v-model="form.mode" name="mode"
|
||||
|
@ -15,7 +17,8 @@
|
|||
native-value="blacklist">Blacklist</b-radio>
|
||||
</div>
|
||||
</b-field>
|
||||
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field v-if="form.mode === 'subscribe'"
|
||||
label="Overwrite?"
|
||||
message="Overwrite name and attribs of existing subscribers?">
|
||||
|
@ -23,6 +26,15 @@
|
|||
<b-switch v-model="form.overwrite" name="overwrite" />
|
||||
</div>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field label="CSV delimiter" message="Default delimiter is comma."
|
||||
class="delimiter">
|
||||
<b-input v-model="form.delim" name="delim"
|
||||
placeholder="," maxlength="1" required />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<list-selector v-if="form.mode === 'subscribe'"
|
||||
label="Lists"
|
||||
|
@ -33,13 +45,8 @@
|
|||
:all="lists.results"
|
||||
></list-selector>
|
||||
<hr />
|
||||
<b-field label="CSV delimiter" message="Default delimiter is comma."
|
||||
class="delimiter">
|
||||
<b-input v-model="form.delim" name="delim"
|
||||
placeholder="," maxlength="1" required />
|
||||
</b-field>
|
||||
|
||||
<b-field label="CSV or ZIP file">
|
||||
<b-field label="CSV or ZIP file" label-position="on-border">
|
||||
<b-upload v-model="form.file" drag-drop expanded required>
|
||||
<div class="has-text-centered section">
|
||||
<p>
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
<form @submit.prevent="onSubmit">
|
||||
<div class="modal-card content" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<p v-if="isEditing" class="has-text-grey-light is-size-7">
|
||||
ID: {{ data.id }} / UUID: {{ data.uuid }}
|
||||
</p>
|
||||
<b-tag v-if="isEditing" :class="[data.type, 'is-pulled-right']">{{ data.type }}</b-tag>
|
||||
<h4 v-if="isEditing">{{ data.name }}</h4>
|
||||
<h4 v-else>New list</h4>
|
||||
|
||||
<p v-if="isEditing" class="has-text-grey is-size-7">
|
||||
ID: {{ data.id }} / UUID: {{ data.uuid }}
|
||||
</p>
|
||||
</header>
|
||||
<section expanded class="modal-card-body">
|
||||
<b-field label="Name">
|
||||
<b-field label="Name" label-position="on-border">
|
||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
||||
placeholder="Name" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Type"
|
||||
<b-field label="Type" label-position="on-border"
|
||||
message="Public lists are open to the world to subscribe
|
||||
and their names may appear on public pages such as the subscription
|
||||
management page.">
|
||||
|
@ -26,7 +26,7 @@
|
|||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Opt-in"
|
||||
<b-field label="Opt-in" label-position="on-border"
|
||||
message="Double opt-in sends an e-mail to the subscriber asking for
|
||||
confirmation. On Double opt-in lists, campaigns are only sent to
|
||||
confirmed subscribers.">
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
</b-table-column>
|
||||
|
||||
<b-table-column field="type" label="Type" sortable>
|
||||
<div>
|
||||
<b-tag :class="props.row.type">{{ props.row.type }}</b-tag>
|
||||
{{ ' ' }}
|
||||
<b-tag>
|
||||
|
@ -38,6 +39,7 @@
|
|||
Send opt-in campaign
|
||||
</b-tooltip>
|
||||
</router-link>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="subscriberCount" label="Subscribers" numeric sortable centered>
|
||||
|
@ -54,6 +56,7 @@
|
|||
</b-table-column>
|
||||
|
||||
<b-table-column class="actions" align="right">
|
||||
<div>
|
||||
<router-link :to="`/campaign/new?list_id=${props.row.id}`">
|
||||
<b-tooltip label="Send campaign" type="is-dark">
|
||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||
|
@ -69,6 +72,7 @@
|
|||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
</div>
|
||||
</b-table-column>
|
||||
</template>
|
||||
|
||||
|
@ -78,7 +82,7 @@
|
|||
</b-table>
|
||||
|
||||
<!-- Add / edit form modal -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="450">
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="600">
|
||||
<list-form :data="curItem" :isEditing="isEditing" @finished="formFinished"></list-form>
|
||||
</b-modal>
|
||||
</section>
|
||||
|
|
|
@ -16,16 +16,16 @@
|
|||
<section class="wrap-small">
|
||||
<form @submit.prevent="onSubmit">
|
||||
<b-tabs type="is-boxed" :animated="false">
|
||||
<b-tab-item label="General">
|
||||
<b-tab-item label="General" label-position="on-border">
|
||||
<div class="items">
|
||||
<b-field label="Logo URL"
|
||||
<b-field label="Logo URL" label-position="on-border"
|
||||
message="(Optional) full URL to the static logo to be displayed on
|
||||
user facing view such as the unsubscription page.">
|
||||
<b-input v-model="form['app.logo_url']" name="app.logo_url"
|
||||
placeholder='https://listmonk.yoursite.com/logo.png' :maxlength="300" />
|
||||
</b-field>
|
||||
|
||||
<b-field label="Favicon URL"
|
||||
<b-field label="Favicon URL" label-position="on-border"
|
||||
message="(Optional) full URL to the static favicon to be displayed on
|
||||
user facing view such as the unsubscription page.">
|
||||
<b-input v-model="form['app.favicon_url']" name="app.favicon_url"
|
||||
|
@ -33,7 +33,7 @@
|
|||
</b-field>
|
||||
|
||||
<hr />
|
||||
<b-field label="Default 'from' email"
|
||||
<b-field label="Default 'from' email" label-position="on-border"
|
||||
message="(Optional) full URL to the static logo to be displayed on
|
||||
user facing view such as the unsubscription page.">
|
||||
<b-input v-model="form['app.from_email']" name="app.from_email"
|
||||
|
@ -41,7 +41,7 @@
|
|||
pattern="(.+?)\s<(.+?)@(.+?)>" :maxlength="300" />
|
||||
</b-field>
|
||||
|
||||
<b-field label="Admin notification e-mails"
|
||||
<b-field label="Admin notification e-mails" label-position="on-border"
|
||||
message="Comma separated list of e-mail addresses to which admin
|
||||
notifications such as import updates, campaign completion,
|
||||
failure etc. should be sent.">
|
||||
|
@ -54,7 +54,7 @@
|
|||
|
||||
<b-tab-item label="Performance">
|
||||
<div class="items">
|
||||
<b-field label="Concurrency"
|
||||
<b-field label="Concurrency" label-position="on-border"
|
||||
message="Maximum concurrent worker (threads) that will attempt to send messages
|
||||
simultaneously.">
|
||||
<b-numberinput v-model="form['app.concurrency']"
|
||||
|
@ -62,7 +62,7 @@
|
|||
placeholder="5" min="1" max="10000" />
|
||||
</b-field>
|
||||
|
||||
<b-field label="Message rate"
|
||||
<b-field label="Message rate" label-position="on-border"
|
||||
message="Maximum number of messages to be sent out per second
|
||||
per worker in a second. If concurrency = 10 and message_rate = 10,
|
||||
then up to 10x10=100 messages may be pushed out every second.
|
||||
|
@ -74,7 +74,7 @@
|
|||
placeholder="5" min="1" max="100000" />
|
||||
</b-field>
|
||||
|
||||
<b-field label="Batch size"
|
||||
<b-field label="Batch size" label-position="on-border"
|
||||
message="The number of subscribers to pull from the databse in a single iteration.
|
||||
Each iteration pulls subscribers from the database, sends messages to them,
|
||||
and then moves on to the next iteration to pull the next batch.
|
||||
|
@ -85,7 +85,7 @@
|
|||
placeholder="1000" min="1" max="100000" />
|
||||
</b-field>
|
||||
|
||||
<b-field label="Maximum error threshold"
|
||||
<b-field label="Maximum error threshold" label-position="on-border"
|
||||
message="The number of errors (eg: SMTP timeouts while e-mailing) a running
|
||||
campaign should tolerate before it is paused for manual
|
||||
investigation or intervention. Set to 0 to never pause.">
|
||||
|
@ -125,7 +125,7 @@
|
|||
|
||||
<b-tab-item label="Media uploads">
|
||||
<div class="items">
|
||||
<b-field label="Provider">
|
||||
<b-field label="Provider" label-position="on-border">
|
||||
<b-select v-model="form['upload.provider']" name="upload.provider">
|
||||
<option value="filesystem">filesystem</option>
|
||||
<option value="s3">s3</option>
|
||||
|
@ -133,14 +133,14 @@
|
|||
</b-field>
|
||||
|
||||
<div class="block" v-if="form['upload.provider'] === 'filesystem'">
|
||||
<b-field label="Upload path"
|
||||
<b-field label="Upload path" label-position="on-border"
|
||||
message="Path to the directory where media will be uploaded.">
|
||||
<b-input v-model="form['upload.filesystem.upload_path']"
|
||||
name="app.upload_path" placeholder='/home/listmonk/uploads'
|
||||
:maxlength="200" />
|
||||
</b-field>
|
||||
|
||||
<b-field label="Upload URI"
|
||||
<b-field label="Upload URI" label-position="on-border"
|
||||
message="Upload URI that's visible to the outside world.
|
||||
The media uploaded to upload_path will be publicly accessible
|
||||
under {root_url}/{}, for instance, https://listmonk.yoursite.com/uploads.">
|
||||
|
@ -150,43 +150,65 @@
|
|||
</div><!-- filesystem -->
|
||||
|
||||
<div class="block" v-if="form['upload.provider'] === 's3'">
|
||||
<b-field label="AWS access key">
|
||||
<b-input v-model="form['upload.s3.aws_access_key_id']"
|
||||
name="upload.s3.aws_access_key_id" :maxlength="200" />
|
||||
</b-field>
|
||||
<b-field label="AWS access secret">
|
||||
<b-input v-model="form['upload.s3.aws_secret_access_key']"
|
||||
name="upload.s3.aws_secret_access_key" type="password" :maxlength="200" />
|
||||
</b-field>
|
||||
<b-field label="Region">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field label="Region" label-position="on-border" expanded>
|
||||
<b-input v-model="form['upload.s3.aws_default_region']"
|
||||
name="upload.s3.aws_default_region"
|
||||
:maxlength="200" placeholder="ap-south-1" />
|
||||
</b-field>
|
||||
<b-field label="Bucket">
|
||||
<b-input v-model="form['upload.s3.bucket']"
|
||||
name="upload.s3.bucket" :maxlength="200" placeholder="" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field label="AWS access key" label-position="on-border" expanded>
|
||||
<b-input v-model="form['upload.s3.aws_access_key_id']"
|
||||
name="upload.s3.aws_access_key_id" :maxlength="200" />
|
||||
</b-field>
|
||||
<b-field label="Bucket path"
|
||||
message="Path inside the bucket to upload files. Default is /">
|
||||
<b-input v-model="form['upload.s3.bucket']"
|
||||
name="upload.s3.bucket_path" :maxlength="200" placeholder="/" />
|
||||
<b-field label="AWS access secret" label-position="on-border" expanded>
|
||||
<b-input v-model="form['upload.s3.aws_secret_access_key']"
|
||||
name="upload.s3.aws_secret_access_key" type="password"
|
||||
:maxlength="200" />
|
||||
</b-field>
|
||||
<b-field label="Bucket type">
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field label="Bucket type" label-position="on-border">
|
||||
<b-select v-model="form['upload.s3.bucket_type']"
|
||||
name="upload.s3.bucket_type">
|
||||
name="upload.s3.bucket_type" expanded>
|
||||
<option value="private">private</option>
|
||||
<option value="public">public</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field label="Upload expiry"
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field label="Bucket" label-position="on-border" expanded>
|
||||
<b-input v-model="form['upload.s3.bucket']"
|
||||
name="upload.s3.bucket" :maxlength="200" placeholder="" />
|
||||
</b-field>
|
||||
<b-field label="Bucket path" label-position="on-border"
|
||||
message="Path inside the bucket to upload files. Default is /" expanded>
|
||||
<b-input v-model="form['upload.s3.bucket_path']"
|
||||
name="upload.s3.bucket_path" :maxlength="200" placeholder="/" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field label="Upload expiry" label-position="on-border"
|
||||
message="(Optional) Specify TTL (in seconds) for the generated presigned URL.
|
||||
Only applicable for private buckets
|
||||
(s, m, h, d for seconds, minutes, hours, days).">
|
||||
(s, m, h, d for seconds, minutes, hours, days)." expanded>
|
||||
<b-input v-model="form['upload.s3.expiry']"
|
||||
name="upload.s3.expiry"
|
||||
placeholder="14d" :pattern="regDuration" :maxlength="10" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- s3 -->
|
||||
</div>
|
||||
</b-tab-item><!-- media -->
|
||||
|
@ -211,14 +233,14 @@
|
|||
<div class="column" :class="{'disabled': !item.enabled}">
|
||||
<div class="columns">
|
||||
<div class="column is-8">
|
||||
<b-field label="Host"
|
||||
<b-field label="Host" label-position="on-border"
|
||||
message="SMTP server's host address.">
|
||||
<b-input v-model="item.host" name="host"
|
||||
placeholder='smtp.yourmailserver.net' :maxlength="200" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field label="Port"
|
||||
<b-field label="Port" label-position="on-border"
|
||||
message="SMTP server's port.">
|
||||
<b-numberinput v-model="item.port" name="port" type="is-light"
|
||||
controls-position="compact"
|
||||
|
@ -229,7 +251,7 @@
|
|||
|
||||
<div class="columns">
|
||||
<div class="column is-2">
|
||||
<b-field label="Auth protocol">
|
||||
<b-field label="Auth protocol" label-position="on-border">
|
||||
<b-select v-model="item.auth_protocol" name="auth_protocol">
|
||||
<option value="none">none</option>
|
||||
<option value="cram">cram</option>
|
||||
|
@ -240,12 +262,12 @@
|
|||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field label="Username" expanded>
|
||||
<b-field label="Username" label-position="on-border" expanded>
|
||||
<b-input v-model="item.username"
|
||||
:disabled="item.auth_protocol === 'none'"
|
||||
name="username" placeholder="mysmtp" :maxlength="200" />
|
||||
</b-field>
|
||||
<b-field label="Password" expanded
|
||||
<b-field label="Password" label-position="on-border" expanded
|
||||
message="Enter a value to change. Otherwise, leave empty.">
|
||||
<b-input v-model="item.password"
|
||||
:disabled="item.auth_protocol === 'none'"
|
||||
|
@ -259,7 +281,7 @@
|
|||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field label="HELO hostname"
|
||||
<b-field label="HELO hostname" label-position="on-border"
|
||||
message="Optional. Some SMTP servers require a FQDN in the hostname.
|
||||
By default, HELLOs go with 'localhost'. Set this if a custom
|
||||
hostname should be used.">
|
||||
|
@ -285,7 +307,7 @@
|
|||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field label="Max. connections"
|
||||
<b-field label="Max. connections" label-position="on-border"
|
||||
message="Maximum concurrent connections to the SMTP server.">
|
||||
<b-numberinput v-model="item.max_conns" name="max_conns" type="is-light"
|
||||
controls-position="compact"
|
||||
|
@ -293,7 +315,7 @@
|
|||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field label="Retries"
|
||||
<b-field label="Retries" label-position="on-border"
|
||||
message="The number of times a message should be retried
|
||||
if sending fails.">
|
||||
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries"
|
||||
|
@ -303,7 +325,7 @@
|
|||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field label="Idle timeout"
|
||||
<b-field label="Idle timeout" label-position="on-border"
|
||||
message="Time to wait for new activity on a connection before closing
|
||||
it and removing it from the pool (s for second, m for minute).">
|
||||
<b-input v-model="item.idle_timeout" name="idle_timeout"
|
||||
|
@ -311,7 +333,7 @@
|
|||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field label="Wait timeout"
|
||||
<b-field label="Wait timeout" label-position="on-border"
|
||||
message="Time to wait for new activity on a connection before closing
|
||||
it and removing it from the pool (s for second, m for minute).">
|
||||
<b-input v-model="item.wait_timeout" name="wait_timeout"
|
||||
|
@ -341,7 +363,7 @@ import { models } from '../constants';
|
|||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
regDuration: '[0-9]+(ms|s|m|h)',
|
||||
regDuration: '[0-9]+(ms|s|m|h|d)',
|
||||
isLoading: true,
|
||||
|
||||
// formCopy is a stringified copy of the original settings against which
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
<form @submit.prevent="onSubmit">
|
||||
<div class="modal-card" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<h4>Manage lists</h4>
|
||||
<p>{{ numSubscribers }} subscriber(s) selected</p>
|
||||
<h4 class="title is-size-5">Manage lists</h4>
|
||||
</header>
|
||||
|
||||
<section expanded class="modal-card-body">
|
||||
|
|
|
@ -12,16 +12,17 @@
|
|||
</p>
|
||||
</header>
|
||||
<section expanded class="modal-card-body">
|
||||
<b-field label="E-mail">
|
||||
<b-field label="E-mail" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.email" :ref="'focus'"
|
||||
placeholder="E-mail" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Name">
|
||||
<b-field label="Name" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.name" placeholder="Name"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Status" message="Blacklisted subscribers will never receive any e-mails.">
|
||||
<b-field label="Status" label-position="on-border"
|
||||
message="Blacklisted subscribers will never receive any e-mails.">
|
||||
<b-select v-model="form.status" placeholder="Status" required>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="blacklisted">Blacklisted</option>
|
||||
|
@ -37,7 +38,7 @@
|
|||
:all="lists.results"
|
||||
></list-selector>
|
||||
|
||||
<b-field label="Attributes"
|
||||
<b-field label="Attributes" label-position="on-border"
|
||||
message='Attributes are defined as a JSON map, for example:
|
||||
{"job": "developer", "location": "Mars", "has_rocket": true}.'>
|
||||
<b-input v-model="form.strAttribs" type="textarea" />
|
||||
|
|
|
@ -140,6 +140,7 @@
|
|||
</b-table-column>
|
||||
|
||||
<b-table-column class="actions" align="right">
|
||||
<div>
|
||||
<a :href="`/api/subscribers/${props.row.id}/export`">
|
||||
<b-tooltip label="Download data" type="is-dark">
|
||||
<b-icon icon="cloud-download-outline" size="is-small" />
|
||||
|
@ -156,6 +157,7 @@
|
|||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
</div>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="empty" v-if="!loading.subscribers">
|
||||
|
@ -170,7 +172,7 @@
|
|||
</b-modal>
|
||||
|
||||
<!-- Add / edit form modal -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="750">
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="600">
|
||||
<subscriber-form :data="curItem" :isEditing="isEditing"
|
||||
@finished="querySubscribers"></subscriber-form>
|
||||
</b-modal>
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
<h4 v-else>New template</h4>
|
||||
</header>
|
||||
<section expanded class="modal-card-body">
|
||||
<b-field label="Name">
|
||||
<b-field label="Name" label-position="on-border">
|
||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
||||
placeholder="Name" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Raw HTML">
|
||||
<b-field label="Raw HTML" label-position="on-border">
|
||||
<b-input v-model="form.body" type="textarea" required />
|
||||
</b-field>
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
</b-table-column>
|
||||
|
||||
<b-table-column class="actions" align="right">
|
||||
<div>
|
||||
<a href="#" @click.prevent="previewTemplate(props.row)">
|
||||
<b-tooltip label="Preview" type="is-dark">
|
||||
<b-icon icon="file-find-outline" size="is-small" />
|
||||
|
@ -51,6 +52,7 @@
|
|||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
</div>
|
||||
</b-table-column>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Reference in New Issue