mirror of
https://github.com/knadh/listmonk.git
synced 2024-09-20 07:16:33 +08:00
Refactor and upgrade the frontend vue code to work with vite instead of webpack.
- Upgrade eslint and fix a massive number (~2500!) of linting errors from new rules. - Upgrade babel core frontend dev dependency. - Upgrade UI lib and other frontend deps. - Refactor the Vue admin app to use `vite` instead of `webpack`. - This was an extremely tedious and painstaking, trial-and-error alchemy job. My disdain for the Javascript "ecosystem" grows. - Re-add custom admin appearance endpoints to the refactored Vue page. - Remove obsolete vue-cli config. - Re-auto-format all .vue files again to work with new linters.
This commit is contained in:
parent
51af75cfef
commit
af8b420d53
6
Makefile
6
Makefile
|
@ -14,8 +14,8 @@ FRONTEND_DIST = frontend/dist
|
|||
FRONTEND_DEPS = \
|
||||
$(FRONTEND_YARN_MODULES) \
|
||||
frontend/package.json \
|
||||
frontend/vue.config.js \
|
||||
frontend/babel.config.js \
|
||||
frontend/vite.config.js \
|
||||
frontend/.eslintrc.js \
|
||||
$(shell find frontend/fontello frontend/public frontend/src -type f)
|
||||
|
||||
BIN := listmonk
|
||||
|
@ -57,7 +57,7 @@ build-frontend: $(FRONTEND_DIST)
|
|||
# Run the JS frontend server in dev mode.
|
||||
.PHONY: run-frontend
|
||||
run-frontend:
|
||||
export VUE_APP_VERSION="${VERSION}" && cd frontend && $(YARN) serve
|
||||
export VUE_APP_VERSION="${VERSION}" && cd frontend && $(YARN) dev
|
||||
|
||||
# Run Go tests.
|
||||
.PHONY: test
|
||||
|
|
25
frontend/.eslintrc.js
vendored
25
frontend/.eslintrc.js
vendored
|
@ -2,16 +2,29 @@ module.exports = {
|
|||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
// es2022: true,
|
||||
},
|
||||
plugins: ['vue'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/essential',
|
||||
'@vue/airbnb',
|
||||
'plugin:vue/strongly-recommended',
|
||||
'@vue/eslint-config-airbnb',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
},
|
||||
parser: 'vue-eslint-parser',
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/quote-props': 'off',
|
||||
'vue/first-attribute-linebreak': 'off',
|
||||
'vue/no-child-content': 'off',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/html-indent': 'off',
|
||||
'vue/html-closing-bracket-newline': 'off',
|
||||
'vue/max-len': ['error', {
|
||||
code: 200,
|
||||
template: 200,
|
||||
comments: 200,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ beforeEach(() => {
|
|||
req.destroy();
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/api/health/**', (req) => {
|
||||
cy.intercept('GET', '/api/health', (req) => {
|
||||
req.reply({});
|
||||
});
|
||||
});
|
||||
|
|
21
frontend/index.html
vendored
Normal file
21
frontend/index.html
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="/admin/static/favicon.png" />
|
||||
<link href="/admin/custom.css" rel="stylesheet" type="text/css">
|
||||
<script src="/admin/custom.js" async defer></script>
|
||||
<title>listmonk</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but listmonk doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
8
frontend/jsconfig.json
vendored
Normal file
8
frontend/jsconfig.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
41
frontend/package.json
vendored
41
frontend/package.json
vendored
|
@ -3,45 +3,42 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"build-report": "vue-cli-service build --report",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore src",
|
||||
"prebuild": "eslint --ext .js,.vue --ignore-path .gitignore src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tinymce/tinymce-vue": "^3",
|
||||
"axios": "^1.6.0",
|
||||
"buefy": "^0.9.10",
|
||||
"axios": "^1.6.2",
|
||||
"buefy": "^0.9.25",
|
||||
"bulma": "^0.9.4",
|
||||
"c3": "^0.7.20",
|
||||
"codeflask": "^1.4.1",
|
||||
"core-js": "^3.12.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"dayjs": "^1.11.10",
|
||||
"indent.js": "^0.3.5",
|
||||
"qs": "^6.10.1",
|
||||
"textversionjs": "^1.1.3",
|
||||
"tinymce": "^5.10.9",
|
||||
"turndown": "^7.0.0",
|
||||
"vue": "^2.6.12",
|
||||
"vue-i18n": "^8.22.2",
|
||||
"turndown": "^7.1.2",
|
||||
"vue": "^2.7.14",
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/eslint-parser": "^7.23.3",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||
"@vue/cli-plugin-router": "~5.0.8",
|
||||
"@vue/cli-plugin-vuex": "~5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/eslint-config-airbnb": "^5.3.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.1",
|
||||
"@vue/eslint-config-airbnb": "^7.0.1",
|
||||
"cypress": "13.6.1",
|
||||
"cypress-file-upload": "^5.0.2",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-define-config": "^2.0.0",
|
||||
"eslint-plugin-import": "^2.23.3",
|
||||
"eslint-plugin-vue": "^7.9.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"sass": "^1.34.0",
|
||||
"sass-loader": "^10.2.0",
|
||||
"vite": "^5.0.10",
|
||||
"vue-eslint-parser": "^9.3.2",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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 %>static/favicon.png" />
|
||||
<link href="<%= BASE_URL %>custom.css" rel="stylesheet" type="text/css">
|
||||
<script src="<%= BASE_URL %>custom.js" async defer></script>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,37 +1,30 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<b-navbar :fixed-top="true" v-if="$root.isLoaded">
|
||||
<template #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 #end>
|
||||
<navigation v-if="isMobile" :isMobile="isMobile"
|
||||
:activeItem="activeItem" :activeGroup="activeGroup" @toggleGroup="toggleGroup"
|
||||
@doLogout="doLogout" />
|
||||
<b-navbar-item v-else tag="div">
|
||||
<a href="#" @click.prevent="doLogout">{{ $t('users.logout') }}</a>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
<template #brand>
|
||||
<div class="logo">
|
||||
<router-link :to="{ name: 'dashboard' }">
|
||||
<img class="full" src="@/assets/logo.svg" alt="" />
|
||||
<img class="favicon" src="@/assets/favicon.png" alt="" />
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #end>
|
||||
<navigation v-if="isMobile" :is-mobile="isMobile" :active-item="activeItem" :active-group="activeGroup"
|
||||
@toggleGroup="toggleGroup" @doLogout="doLogout" />
|
||||
<b-navbar-item v-else tag="div">
|
||||
<a href="#" @click.prevent="doLogout">{{ $t('users.logout') }}</a>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
</b-navbar>
|
||||
|
||||
<div class="wrapper" v-if="$root.isLoaded">
|
||||
<section class="sidebar">
|
||||
<b-sidebar
|
||||
position="static"
|
||||
mobile="hide"
|
||||
:fullheight="true"
|
||||
:open="true"
|
||||
:can-cancel="false"
|
||||
>
|
||||
<b-sidebar position="static" mobile="hide" :fullheight="true" :open="true" :can-cancel="false">
|
||||
<div>
|
||||
<b-menu :accordion="false">
|
||||
<navigation v-if="!isMobile" :isMobile="isMobile"
|
||||
:activeItem="activeItem" :activeGroup="activeGroup" @toggleGroup="toggleGroup" />
|
||||
<navigation v-if="!isMobile" :is-mobile="isMobile" :active-item="activeItem" :active-group="activeGroup"
|
||||
@toggleGroup="toggleGroup" />
|
||||
</b-menu>
|
||||
</div>
|
||||
</b-sidebar>
|
||||
|
@ -43,15 +36,15 @@
|
|||
<div class="global-notices" v-if="serverConfig.needs_restart || serverConfig.update">
|
||||
<div v-if="serverConfig.needs_restart" class="notification is-danger">
|
||||
{{ $t('settings.needsRestart') }}
|
||||
—
|
||||
—
|
||||
<b-button class="is-primary" size="is-small"
|
||||
@click="$utils.confirm($t('settings.confirmRestart'), reloadApp)">
|
||||
{{ $t('settings.restart') }}
|
||||
{{ $t('settings.restart') }}
|
||||
</b-button>
|
||||
</div>
|
||||
<div v-if="serverConfig.update" class="notification is-success">
|
||||
{{ $t('settings.updateAvailable', { version: serverConfig.update.version }) }}
|
||||
<a :href="serverConfig.update.url" target="_blank">View</a>
|
||||
<a :href="serverConfig.update.url" target="_blank" rel="noopener noreferer">View</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -157,7 +150,7 @@ export default Vue.extend({
|
|||
...mapState(['serverConfig']),
|
||||
|
||||
version() {
|
||||
return process.env.VUE_APP_VERSION;
|
||||
return import.meta.env.VUE_APP_VERSION;
|
||||
},
|
||||
|
||||
isMobile() {
|
||||
|
@ -180,6 +173,6 @@ export default Vue.extend({
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "assets/style.scss";
|
||||
@import "assets/icons/fontello.css";
|
||||
@import "assets/style.scss";
|
||||
@import "assets/icons/fontello.css";
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { models } from '../constants';
|
|||
import Utils from '../utils';
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: process.env.VUE_APP_ROOT_URL || '/',
|
||||
baseURL: import.meta.env.VUE_APP_ROOT_URL || '/',
|
||||
withCredentials: false,
|
||||
responseType: 'json',
|
||||
|
||||
|
@ -95,111 +95,176 @@ http.interceptors.response.use((resp) => {
|
|||
// store: modelName (set's the API response in the global store. eg: store.lists: { ... } )
|
||||
|
||||
// Health check endpoint that does not throw a toast.
|
||||
export const getHealth = () => http.get('/api/health',
|
||||
{ disableToast: true });
|
||||
export const getHealth = () => http.get(
|
||||
'/api/health',
|
||||
{ disableToast: true },
|
||||
);
|
||||
|
||||
export const reloadApp = () => http.post('/api/admin/reload');
|
||||
|
||||
// Dashboard
|
||||
export const getDashboardCounts = () => http.get('/api/dashboard/counts',
|
||||
{ loading: models.dashboard });
|
||||
export const getDashboardCounts = () => http.get(
|
||||
'/api/dashboard/counts',
|
||||
{ loading: models.dashboard },
|
||||
);
|
||||
|
||||
export const getDashboardCharts = () => http.get('/api/dashboard/charts',
|
||||
{ loading: models.dashboard });
|
||||
export const getDashboardCharts = () => http.get(
|
||||
'/api/dashboard/charts',
|
||||
{ loading: models.dashboard },
|
||||
);
|
||||
|
||||
// Lists.
|
||||
export const getLists = (params) => http.get('/api/lists',
|
||||
export const getLists = (params) => http.get(
|
||||
'/api/lists',
|
||||
{
|
||||
params: (!params ? { per_page: 'all' } : params),
|
||||
loading: models.lists,
|
||||
store: models.lists,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const queryLists = (params) => http.get('/api/lists',
|
||||
export const queryLists = (params) => http.get(
|
||||
'/api/lists',
|
||||
{
|
||||
params: (!params ? { per_page: 'all' } : params),
|
||||
loading: models.lists,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const getList = async (id) => http.get(`/api/lists/${id}`,
|
||||
{ loading: models.list });
|
||||
export const getList = async (id) => http.get(
|
||||
`/api/lists/${id}`,
|
||||
{ loading: models.list },
|
||||
);
|
||||
|
||||
export const createList = (data) => http.post('/api/lists', data,
|
||||
{ loading: models.lists });
|
||||
export const createList = (data) => http.post(
|
||||
'/api/lists',
|
||||
data,
|
||||
{ loading: models.lists },
|
||||
);
|
||||
|
||||
export const updateList = (data) => http.put(`/api/lists/${data.id}`, data,
|
||||
{ loading: models.lists });
|
||||
export const updateList = (data) => http.put(
|
||||
`/api/lists/${data.id}`,
|
||||
data,
|
||||
{ loading: models.lists },
|
||||
);
|
||||
|
||||
export const deleteList = (id) => http.delete(`/api/lists/${id}`,
|
||||
{ loading: models.lists });
|
||||
export const deleteList = (id) => http.delete(
|
||||
`/api/lists/${id}`,
|
||||
{ loading: models.lists },
|
||||
);
|
||||
|
||||
// Subscribers.
|
||||
export const getSubscribers = async (params) => http.get('/api/subscribers',
|
||||
export const getSubscribers = async (params) => http.get(
|
||||
'/api/subscribers',
|
||||
{
|
||||
params,
|
||||
loading: models.subscribers,
|
||||
store: models.subscribers,
|
||||
camelCase: (keyPath) => !keyPath.startsWith('.results.*.attribs'),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const getSubscriber = async (id) => http.get(`/api/subscribers/${id}`,
|
||||
{ loading: models.subscribers });
|
||||
export const getSubscriber = async (id) => http.get(
|
||||
`/api/subscribers/${id}`,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const getSubscriberBounces = async (id) => http.get(`/api/subscribers/${id}/bounces`,
|
||||
{ loading: models.bounces });
|
||||
export const getSubscriberBounces = async (id) => http.get(
|
||||
`/api/subscribers/${id}/bounces`,
|
||||
{ loading: models.bounces },
|
||||
);
|
||||
|
||||
export const deleteSubscriberBounces = async (id) => http.delete(`/api/subscribers/${id}/bounces`,
|
||||
{ loading: models.bounces });
|
||||
export const deleteSubscriberBounces = async (id) => http.delete(
|
||||
`/api/subscribers/${id}/bounces`,
|
||||
{ loading: models.bounces },
|
||||
);
|
||||
|
||||
export const deleteBounce = async (id) => http.delete(`/api/bounces/${id}`,
|
||||
{ loading: models.bounces });
|
||||
export const deleteBounce = async (id) => http.delete(
|
||||
`/api/bounces/${id}`,
|
||||
{ loading: models.bounces },
|
||||
);
|
||||
|
||||
export const deleteBounces = async (params) => http.delete('/api/bounces',
|
||||
{ params, loading: models.bounces });
|
||||
export const deleteBounces = async (params) => http.delete(
|
||||
'/api/bounces',
|
||||
{ params, loading: models.bounces },
|
||||
);
|
||||
|
||||
export const createSubscriber = (data) => http.post('/api/subscribers', data,
|
||||
{ loading: models.subscribers });
|
||||
export const createSubscriber = (data) => http.post(
|
||||
'/api/subscribers',
|
||||
data,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const updateSubscriber = (data) => http.put(`/api/subscribers/${data.id}`, data,
|
||||
{ loading: models.subscribers });
|
||||
export const updateSubscriber = (data) => http.put(
|
||||
`/api/subscribers/${data.id}`,
|
||||
data,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const sendSubscriberOptin = (id) => http.post(`/api/subscribers/${id}/optin`, {},
|
||||
{ loading: models.subscribers });
|
||||
export const sendSubscriberOptin = (id) => http.post(
|
||||
`/api/subscribers/${id}/optin`,
|
||||
{},
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const deleteSubscriber = (id) => http.delete(`/api/subscribers/${id}`,
|
||||
{ loading: models.subscribers });
|
||||
export const deleteSubscriber = (id) => http.delete(
|
||||
`/api/subscribers/${id}`,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const addSubscribersToLists = (data) => http.put('/api/subscribers/lists', data,
|
||||
{ loading: models.subscribers });
|
||||
export const addSubscribersToLists = (data) => http.put(
|
||||
'/api/subscribers/lists',
|
||||
data,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const addSubscribersToListsByQuery = (data) => http.put('/api/subscribers/query/lists',
|
||||
data, { loading: models.subscribers });
|
||||
export const addSubscribersToListsByQuery = (data) => http.put(
|
||||
'/api/subscribers/query/lists',
|
||||
data,
|
||||
|
||||
export const blocklistSubscribers = (data) => http.put('/api/subscribers/blocklist', data,
|
||||
{ loading: models.subscribers });
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const blocklistSubscribersByQuery = (data) => http.put('/api/subscribers/query/blocklist', data,
|
||||
{ loading: models.subscribers });
|
||||
export const blocklistSubscribers = (data) => http.put(
|
||||
'/api/subscribers/blocklist',
|
||||
data,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const deleteSubscribers = (params) => http.delete('/api/subscribers',
|
||||
{ params, loading: models.subscribers });
|
||||
export const blocklistSubscribersByQuery = (data) => http.put(
|
||||
'/api/subscribers/query/blocklist',
|
||||
data,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const deleteSubscribersByQuery = (data) => http.post('/api/subscribers/query/delete', data,
|
||||
{ loading: models.subscribers });
|
||||
export const deleteSubscribers = (params) => http.delete(
|
||||
'/api/subscribers',
|
||||
{ params, loading: models.subscribers },
|
||||
);
|
||||
|
||||
export const deleteSubscribersByQuery = (data) => http.post(
|
||||
'/api/subscribers/query/delete',
|
||||
data,
|
||||
{ loading: models.subscribers },
|
||||
);
|
||||
|
||||
// Subscriber import.
|
||||
export const importSubscribers = (data) => http.post('/api/import/subscribers', data);
|
||||
|
||||
export const getImportStatus = () => http.get('/api/import/subscribers');
|
||||
|
||||
export const getImportLogs = async () => http.get('/api/import/subscribers/logs',
|
||||
{ camelCase: false });
|
||||
export const getImportLogs = async () => http.get(
|
||||
'/api/import/subscribers/logs',
|
||||
{ camelCase: false },
|
||||
);
|
||||
|
||||
export const stopImport = () => http.delete('/api/import/subscribers');
|
||||
|
||||
// Bounces.
|
||||
export const getBounces = async (params) => http.get('/api/bounces',
|
||||
{ params, loading: models.bounces });
|
||||
export const getBounces = async (params) => http.get(
|
||||
'/api/bounces',
|
||||
{ params, loading: models.bounces },
|
||||
);
|
||||
|
||||
// Campaigns.
|
||||
export const getCampaigns = async (params) => http.get('/api/campaigns', {
|
||||
|
@ -216,93 +281,162 @@ export const getCampaign = async (id) => http.get(`/api/campaigns/${id}`, {
|
|||
|
||||
export const getCampaignStats = async () => http.get('/api/campaigns/running/stats', {});
|
||||
|
||||
export const createCampaign = async (data) => http.post('/api/campaigns', data,
|
||||
{ loading: models.campaigns });
|
||||
export const createCampaign = async (data) => http.post(
|
||||
'/api/campaigns',
|
||||
data,
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const getCampaignViewCounts = async (params) => http.get('/api/campaigns/analytics/views',
|
||||
{ params, loading: models.campaigns });
|
||||
export const getCampaignViewCounts = async (params) => http.get(
|
||||
'/api/campaigns/analytics/views',
|
||||
{ params, loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const getCampaignClickCounts = async (params) => http.get('/api/campaigns/analytics/clicks',
|
||||
{ params, loading: models.campaigns });
|
||||
export const getCampaignClickCounts = async (params) => http.get(
|
||||
'/api/campaigns/analytics/clicks',
|
||||
{ params, loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const getCampaignBounceCounts = async (params) => http.get('/api/campaigns/analytics/bounces',
|
||||
{ params, loading: models.campaigns });
|
||||
export const getCampaignBounceCounts = async (params) => http.get(
|
||||
'/api/campaigns/analytics/bounces',
|
||||
{ params, loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const getCampaignLinkCounts = async (params) => http.get('/api/campaigns/analytics/links',
|
||||
{ params, loading: models.campaigns });
|
||||
export const getCampaignLinkCounts = async (params) => http.get(
|
||||
'/api/campaigns/analytics/links',
|
||||
{ params, loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const convertCampaignContent = async (data) => http.post(`/api/campaigns/${data.id}/content`, data,
|
||||
{ loading: models.campaigns });
|
||||
export const convertCampaignContent = async (data) => http.post(
|
||||
`/api/campaigns/${data.id}/content`,
|
||||
data,
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const testCampaign = async (data) => http.post(`/api/campaigns/${data.id}/test`, data,
|
||||
{ loading: models.campaigns });
|
||||
export const testCampaign = async (data) => http.post(
|
||||
`/api/campaigns/${data.id}/test`,
|
||||
data,
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const updateCampaign = async (id, data) => http.put(`/api/campaigns/${id}`, data,
|
||||
{ loading: models.campaigns });
|
||||
export const updateCampaign = async (id, data) => http.put(
|
||||
`/api/campaigns/${id}`,
|
||||
data,
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const changeCampaignStatus = async (id, status) => http.put(`/api/campaigns/${id}/status`,
|
||||
{ status }, { loading: models.campaigns });
|
||||
export const changeCampaignStatus = async (id, status) => http.put(
|
||||
`/api/campaigns/${id}/status`,
|
||||
{ status },
|
||||
|
||||
export const updateCampaignArchive = async (id, data) => http.put(`/api/campaigns/${id}/archive`, data,
|
||||
{ loading: models.campaigns });
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const deleteCampaign = async (id) => http.delete(`/api/campaigns/${id}`,
|
||||
{ loading: models.campaigns });
|
||||
export const updateCampaignArchive = async (id, data) => http.put(
|
||||
`/api/campaigns/${id}/archive`,
|
||||
data,
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
export const deleteCampaign = async (id) => http.delete(
|
||||
`/api/campaigns/${id}`,
|
||||
{ loading: models.campaigns },
|
||||
);
|
||||
|
||||
// Media.
|
||||
export const getMedia = async (params) => http.get('/api/media',
|
||||
{ params, loading: models.media, store: models.media });
|
||||
export const getMedia = async (params) => http.get(
|
||||
'/api/media',
|
||||
{ params, loading: models.media, store: models.media },
|
||||
);
|
||||
|
||||
export const uploadMedia = (data) => http.post('/api/media', data,
|
||||
{ loading: models.media });
|
||||
export const uploadMedia = (data) => http.post(
|
||||
'/api/media',
|
||||
data,
|
||||
{ loading: models.media },
|
||||
);
|
||||
|
||||
export const deleteMedia = (id) => http.delete(`/api/media/${id}`,
|
||||
{ loading: models.media });
|
||||
export const deleteMedia = (id) => http.delete(
|
||||
`/api/media/${id}`,
|
||||
{ loading: models.media },
|
||||
);
|
||||
|
||||
// Templates.
|
||||
export const createTemplate = async (data) => http.post('/api/templates', data,
|
||||
{ loading: models.templates });
|
||||
export const createTemplate = async (data) => http.post(
|
||||
'/api/templates',
|
||||
data,
|
||||
{ loading: models.templates },
|
||||
);
|
||||
|
||||
export const getTemplates = async () => http.get('/api/templates',
|
||||
{ loading: models.templates, store: models.templates });
|
||||
export const getTemplates = async () => http.get(
|
||||
'/api/templates',
|
||||
{ loading: models.templates, store: models.templates },
|
||||
);
|
||||
|
||||
export const updateTemplate = async (data) => http.put(`/api/templates/${data.id}`, data,
|
||||
{ loading: models.templates });
|
||||
export const updateTemplate = async (data) => http.put(
|
||||
`/api/templates/${data.id}`,
|
||||
data,
|
||||
{ loading: models.templates },
|
||||
);
|
||||
|
||||
export const makeTemplateDefault = async (id) => http.put(`/api/templates/${id}/default`, {},
|
||||
{ loading: models.templates });
|
||||
export const makeTemplateDefault = async (id) => http.put(
|
||||
`/api/templates/${id}/default`,
|
||||
{},
|
||||
{ loading: models.templates },
|
||||
);
|
||||
|
||||
export const deleteTemplate = async (id) => http.delete(`/api/templates/${id}`,
|
||||
{ loading: models.templates });
|
||||
export const deleteTemplate = async (id) => http.delete(
|
||||
`/api/templates/${id}`,
|
||||
{ loading: models.templates },
|
||||
);
|
||||
|
||||
// Settings.
|
||||
export const getServerConfig = async () => http.get('/api/config',
|
||||
{ loading: models.serverConfig, store: models.serverConfig, camelCase: false });
|
||||
export const getServerConfig = async () => http.get(
|
||||
'/api/config',
|
||||
{ loading: models.serverConfig, store: models.serverConfig, camelCase: false },
|
||||
);
|
||||
|
||||
export const getSettings = async () => http.get('/api/settings',
|
||||
{ loading: models.settings, store: models.settings, camelCase: false });
|
||||
export const getSettings = async () => http.get(
|
||||
'/api/settings',
|
||||
{ loading: models.settings, store: models.settings, camelCase: false },
|
||||
);
|
||||
|
||||
export const updateSettings = async (data) => http.put('/api/settings', data,
|
||||
{ loading: models.settings });
|
||||
export const updateSettings = async (data) => http.put(
|
||||
'/api/settings',
|
||||
data,
|
||||
{ loading: models.settings },
|
||||
);
|
||||
|
||||
export const testSMTP = async (data) => http.post('/api/settings/smtp/test', data,
|
||||
{ loading: models.settings, disableToast: true });
|
||||
export const testSMTP = async (data) => http.post(
|
||||
'/api/settings/smtp/test',
|
||||
data,
|
||||
{ loading: models.settings, disableToast: true },
|
||||
);
|
||||
|
||||
export const getLogs = async () => http.get('/api/logs',
|
||||
{ loading: models.logs, camelCase: false });
|
||||
export const getLogs = async () => http.get(
|
||||
'/api/logs',
|
||||
{ loading: models.logs, camelCase: false },
|
||||
);
|
||||
|
||||
export const getLang = async (lang) => http.get(`/api/lang/${lang}`,
|
||||
{ loading: models.lang, camelCase: false });
|
||||
export const getLang = async (lang) => http.get(
|
||||
`/api/lang/${lang}`,
|
||||
{ loading: models.lang, camelCase: false },
|
||||
);
|
||||
|
||||
export const logout = async () => http.get('/api/logout', {
|
||||
auth: { username: 'wrong', password: 'wrong' },
|
||||
});
|
||||
|
||||
export const deleteGCCampaignAnalytics = async (typ, beforeDate) => http.delete(`/api/maintenance/analytics/${typ}`,
|
||||
{ loading: models.maintenance, params: { before_date: beforeDate } });
|
||||
export const deleteGCCampaignAnalytics = async (typ, beforeDate) => http.delete(
|
||||
`/api/maintenance/analytics/${typ}`,
|
||||
{ loading: models.maintenance, params: { before_date: beforeDate } },
|
||||
);
|
||||
|
||||
export const deleteGCSubscribers = async (typ) => http.delete(`/api/maintenance/subscribers/${typ}`,
|
||||
{ loading: models.maintenance });
|
||||
export const deleteGCSubscribers = async (typ) => http.delete(
|
||||
`/api/maintenance/subscribers/${typ}`,
|
||||
{ loading: models.maintenance },
|
||||
);
|
||||
|
||||
export const deleteGCSubscriptions = async (beforeDate) => http.delete('/api/maintenance/subscriptions/unconfirmed',
|
||||
{ loading: models.maintenance, params: { before_date: beforeDate } });
|
||||
export const deleteGCSubscriptions = async (beforeDate) => http.delete(
|
||||
'/api/maintenance/subscriptions/unconfirmed',
|
||||
{ loading: models.maintenance, params: { before_date: beforeDate } },
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* Import Bulma to set variables */
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
@import "../node_modules/bulma/sass/utilities/_all";
|
||||
|
||||
/* import inter-regular */
|
||||
@font-face {
|
||||
|
@ -49,8 +49,8 @@ $menu-item-active-color: $primary;
|
|||
$modal-background-background-color: rgba(0, 0, 0, .30);
|
||||
|
||||
/* Import full Bulma and Buefy */
|
||||
@import "~bulma";
|
||||
@import "~buefy/src/scss/buefy";
|
||||
@import "bulma";
|
||||
@import "buefy/src/scss/buefy";
|
||||
|
||||
/* Custom style overrides */
|
||||
html, body {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-modal scroll="keep" @close="close"
|
||||
:aria-modal="true" :active="isVisible">
|
||||
<b-modal scroll="keep" @close="close" :aria-modal="true" :active="isVisible">
|
||||
<div>
|
||||
<div class="modal-card" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
|
@ -9,7 +8,7 @@
|
|||
</header>
|
||||
</div>
|
||||
<section expanded class="modal-card-body preview">
|
||||
<b-loading :active="isLoading" :is-full-page="false"></b-loading>
|
||||
<b-loading :active="isLoading" :is-full-page="false" />
|
||||
<form v-if="body" method="post" :action="previewURL" target="iframe" ref="form">
|
||||
<input type="hidden" name="template_id" :value="templateId" />
|
||||
<input type="hidden" name="content_type" :value="contentType" />
|
||||
|
@ -17,14 +16,13 @@
|
|||
<input type="hidden" name="body" :value="body" />
|
||||
</form>
|
||||
|
||||
<iframe id="iframe" name="iframe" ref="iframe"
|
||||
:title="title"
|
||||
:src="body ? 'about:blank' : previewURL"
|
||||
@load="onLoaded"
|
||||
></iframe>
|
||||
<iframe id="iframe" name="iframe" ref="iframe" :title="title" :src="body ? 'about:blank' : previewURL"
|
||||
@load="onLoaded" />
|
||||
</section>
|
||||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="close">{{ $t('globals.buttons.close') }}</b-button>
|
||||
<b-button @click="close">
|
||||
{{ $t('globals.buttons.close') }}
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</b-modal>
|
||||
|
@ -39,21 +37,18 @@ export default {
|
|||
|
||||
props: {
|
||||
// Template or campaign ID.
|
||||
id: Number,
|
||||
title: String,
|
||||
id: { type: Number, default: 0 },
|
||||
title: { type: String, default: '' },
|
||||
|
||||
// campaign | template.
|
||||
type: String,
|
||||
type: { type: String, default: '' },
|
||||
|
||||
// campaign | tx.
|
||||
templateType: String,
|
||||
templateType: { type: String, default: '' },
|
||||
|
||||
body: String,
|
||||
contentType: String,
|
||||
templateId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
body: { type: String, default: '' },
|
||||
contentType: { type: String, default: '' },
|
||||
templateId: { type: Number, default: 0 },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -5,52 +5,48 @@
|
|||
<div class="column is-6">
|
||||
<b-field label="Format">
|
||||
<div>
|
||||
<b-radio v-model="form.radioFormat"
|
||||
@input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="richtext"
|
||||
data-cy="check-richtext">{{ $t('campaigns.richText') }}</b-radio>
|
||||
<b-radio v-model="form.radioFormat" @input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="richtext" data-cy="check-richtext">
|
||||
{{ $t('campaigns.richText') }}
|
||||
</b-radio>
|
||||
|
||||
<b-radio v-model="form.radioFormat"
|
||||
@input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="html"
|
||||
data-cy="check-html">{{ $t('campaigns.rawHTML') }}</b-radio>
|
||||
<b-radio v-model="form.radioFormat" @input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="html" data-cy="check-html">
|
||||
{{ $t('campaigns.rawHTML') }}
|
||||
</b-radio>
|
||||
|
||||
<b-radio v-model="form.radioFormat"
|
||||
@input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="markdown"
|
||||
data-cy="check-markdown">{{ $t('campaigns.markdown') }}</b-radio>
|
||||
<b-radio v-model="form.radioFormat" @input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="markdown" data-cy="check-markdown">
|
||||
{{ $t('campaigns.markdown') }}
|
||||
</b-radio>
|
||||
|
||||
<b-radio v-model="form.radioFormat"
|
||||
@input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="plain"
|
||||
data-cy="check-plain">{{ $t('campaigns.plainText') }}</b-radio>
|
||||
<b-radio v-model="form.radioFormat" @input="onFormatChange" :disabled="disabled" name="format"
|
||||
native-value="plain" data-cy="check-plain">
|
||||
{{ $t('campaigns.plainText') }}
|
||||
</b-radio>
|
||||
</div>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-6 has-text-right">
|
||||
<b-button @click="onTogglePreview" type="is-primary"
|
||||
icon-left="file-find-outline" data-cy="btn-preview">
|
||||
{{ $t('campaigns.preview') }}
|
||||
</b-button>
|
||||
<b-button @click="onTogglePreview" type="is-primary" icon-left="file-find-outline" data-cy="btn-preview">
|
||||
{{ $t('campaigns.preview') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- wsywig //-->
|
||||
<template v-if="isRichtextReady && form.format === 'richtext'">
|
||||
<tiny-mce
|
||||
v-model="form.body"
|
||||
:disabled="disabled"
|
||||
:init="richtextConf"
|
||||
/>
|
||||
<tiny-mce v-model="form.body" :disabled="disabled" :init="richtextConf" />
|
||||
|
||||
<b-modal scroll="keep" :width="1200"
|
||||
:aria-modal="true" :active.sync="isRichtextSourceVisible">
|
||||
<b-modal scroll="keep" :width="1200" :aria-modal="true" :active.sync="isRichtextSourceVisible">
|
||||
<div>
|
||||
<section expanded class="modal-card-body preview">
|
||||
<html-editor v-model="richTextSourceBody" />
|
||||
</section>
|
||||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="onFormatRichtextHTML">{{ $t('campaigns.formatHTML') }}</b-button>
|
||||
<b-button @click="onFormatRichtextHTML">
|
||||
{{ $t('campaigns.formatHTML') }}
|
||||
</b-button>
|
||||
<b-button @click="() => { this.isRichtextSourceVisible = false; }">
|
||||
{{ $t('globals.buttons.close') }}
|
||||
</b-button>
|
||||
|
@ -61,14 +57,15 @@
|
|||
</div>
|
||||
</b-modal>
|
||||
|
||||
<b-modal scroll="keep" :width="750"
|
||||
:aria-modal="true" :active.sync="isInsertHTMLVisible">
|
||||
<b-modal scroll="keep" :width="750" :aria-modal="true" :active.sync="isInsertHTMLVisible">
|
||||
<div>
|
||||
<section expanded class="modal-card-body preview">
|
||||
<html-editor v-model="insertHTMLSnippet" />
|
||||
</section>
|
||||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="onFormatRichtextHTML">{{ $t('campaigns.formatHTML') }}</b-button>
|
||||
<b-button @click="onFormatRichtextHTML">
|
||||
{{ $t('campaigns.formatHTML') }}
|
||||
</b-button>
|
||||
<b-button @click="() => { this.isInsertHTMLVisible = false; }">
|
||||
{{ $t('globals.buttons.close') }}
|
||||
</b-button>
|
||||
|
@ -84,19 +81,12 @@
|
|||
<html-editor v-if="form.format === 'html'" v-model="form.body" />
|
||||
|
||||
<!-- plain text / markdown editor //-->
|
||||
<b-input v-if="form.format === 'plain' || form.format === 'markdown'"
|
||||
v-model="form.body" @input="onEditorChange"
|
||||
<b-input v-if="form.format === 'plain' || form.format === 'markdown'" v-model="form.body" @input="onEditorChange"
|
||||
type="textarea" name="content" ref="plainEditor" class="plain-editor" />
|
||||
|
||||
<!-- campaign preview //-->
|
||||
<campaign-preview v-if="isPreviewing"
|
||||
@close="onTogglePreview"
|
||||
type="campaign"
|
||||
:id="id"
|
||||
:title="title"
|
||||
:contentType="form.format"
|
||||
:templateId="templateId"
|
||||
:body="form.body"></campaign-preview>
|
||||
<campaign-preview v-if="isPreviewing" @close="onTogglePreview" type="campaign" :id="id" :title="title"
|
||||
:content-type="form.format" :template-id="templateId" :body="form.body" />
|
||||
|
||||
<!-- image picker -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isMediaVisible" :width="900">
|
||||
|
@ -110,17 +100,16 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import TurndownService from 'turndown';
|
||||
import { indent } from 'indent.js';
|
||||
import TurndownService from 'turndown';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
import TinyMce from '@tinymce/tinymce-vue';
|
||||
import 'tinymce';
|
||||
import 'tinymce/icons/default';
|
||||
import 'tinymce/themes/silver';
|
||||
import 'tinymce/skins/ui/oxide/skin.css';
|
||||
import 'tinymce/plugins/anchor';
|
||||
import 'tinymce/plugins/autoresize';
|
||||
import 'tinymce/plugins/autolink';
|
||||
import 'tinymce/plugins/autoresize';
|
||||
import 'tinymce/plugins/charmap';
|
||||
import 'tinymce/plugins/colorpicker';
|
||||
import 'tinymce/plugins/contextmenu';
|
||||
|
@ -140,12 +129,13 @@ import 'tinymce/plugins/textcolor';
|
|||
import 'tinymce/plugins/visualblocks';
|
||||
import 'tinymce/plugins/visualchars';
|
||||
import 'tinymce/plugins/wordcount';
|
||||
import TinyMce from '@tinymce/tinymce-vue';
|
||||
import 'tinymce/skins/ui/oxide/skin.css';
|
||||
import 'tinymce/themes/silver';
|
||||
|
||||
import { colors, uris } from '../constants';
|
||||
import Media from '../views/Media.vue';
|
||||
import CampaignPreview from './CampaignPreview.vue';
|
||||
import HTMLEditor from './HTMLEditor.vue';
|
||||
import Media from '../views/Media.vue';
|
||||
import { colors, uris } from '../constants';
|
||||
|
||||
const turndown = new TurndownService();
|
||||
|
||||
|
@ -172,15 +162,12 @@ export default {
|
|||
},
|
||||
|
||||
props: {
|
||||
id: Number,
|
||||
title: String,
|
||||
body: String,
|
||||
contentType: String,
|
||||
templateId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
disabled: Boolean,
|
||||
id: { type: Number, default: 0 },
|
||||
title: { type: String, default: '' },
|
||||
body: { type: String, default: '' },
|
||||
contentType: { type: String, default: '' },
|
||||
templateId: { type: Number, default: 0 },
|
||||
disabled: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>
|
||||
<b-icon :icon="!icon ? 'plus' : icon" size="is-large" />
|
||||
</p>
|
||||
<p>{{ !label ? $t('globals.messages.emptyState') : label }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>
|
||||
<b-icon :icon="!icon ? 'plus' : icon" size="is-large" />
|
||||
</p>
|
||||
<p>{{ !label ? $t('globals.messages.emptyState') : label }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -14,8 +14,8 @@ export default {
|
|||
name: 'EmptyPlaceholder',
|
||||
|
||||
props: {
|
||||
icon: String,
|
||||
label: String,
|
||||
icon: { type: String, default: '' },
|
||||
label: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="htmlEditor" id="html-editor" class="html-editor"></div>
|
||||
<div ref="htmlEditor" id="html-editor" class="html-editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -8,11 +8,8 @@ import { colors } from '../constants';
|
|||
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
language: {
|
||||
type: String,
|
||||
default: 'html',
|
||||
},
|
||||
value: { type: String, default: '' },
|
||||
language: { type: String, default: 'html' },
|
||||
disabled: Boolean,
|
||||
},
|
||||
|
||||
|
|
|
@ -1,34 +1,19 @@
|
|||
<template>
|
||||
<div class="field list-selector">
|
||||
<div :class="['list-tags', ...classes]">
|
||||
<b-taglist>
|
||||
<b-tag v-for="l in selectedItems"
|
||||
:key="l.id"
|
||||
:class="l.subscriptionStatus"
|
||||
:closable="!$props.disabled"
|
||||
:data-id="l.id"
|
||||
@close="removeList(l.id)" class="list">
|
||||
{{ l.name }} <sup v-if="l.optin === 'double'">{{ l.subscriptionStatus }}</sup>
|
||||
</b-tag>
|
||||
</b-taglist>
|
||||
</div>
|
||||
<div :class="['list-tags', ...classes]">
|
||||
<b-taglist>
|
||||
<b-tag v-for="l in selectedItems" :key="l.id" :class="l.subscriptionStatus" :closable="!$props.disabled"
|
||||
:data-id="l.id" @close="removeList(l.id)" class="list">
|
||||
{{ l.name }} <sup v-if="l.optin === 'double'">{{ l.subscriptionStatus }}</sup>
|
||||
</b-tag>
|
||||
</b-taglist>
|
||||
</div>
|
||||
|
||||
<b-field :message="message"
|
||||
:label="label + (selectedItems ? ` (${selectedItems.length})` : '')"
|
||||
<b-field :message="message" :label="label + (selectedItems ? ` (${selectedItems.length})` : '')"
|
||||
label-position="on-border">
|
||||
<b-autocomplete
|
||||
v-model="query"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
dropdown-position="top"
|
||||
:disabled="all.length === 0 || $props.disabled"
|
||||
:keep-first="true"
|
||||
:clear-on-select="true"
|
||||
:open-on-focus="true"
|
||||
:data="filteredLists"
|
||||
@select="selectList"
|
||||
field="name">
|
||||
</b-autocomplete>
|
||||
<b-autocomplete v-model="query" :placeholder="placeholder" clearable dropdown-position="top"
|
||||
:disabled="all.length === 0 || $props.disabled" :keep-first="true" :clear-on-select="true" :open-on-focus="true"
|
||||
:data="filteredLists" @select="selectList" field="name" />
|
||||
</b-field>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -40,9 +25,9 @@ export default {
|
|||
name: 'ListSelector',
|
||||
|
||||
props: {
|
||||
label: String,
|
||||
placeholder: String,
|
||||
message: String,
|
||||
label: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
message: { type: String, default: '' },
|
||||
required: Boolean,
|
||||
disabled: Boolean,
|
||||
classes: {
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
<template>
|
||||
<section class="log-view">
|
||||
<b-loading :active="loading" :is-full-page="false" />
|
||||
<div class="lines" ref="lines">
|
||||
<template v-for="(l, i) in lines">
|
||||
<span :set="line = splitLine(l)" :key="i" class="line">
|
||||
<span class="timestamp" :title="line.file">{{ line.timestamp }}</span>
|
||||
<span class="log-message">{{ line.message }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
<section class="log-view">
|
||||
<b-loading :active="loading" :is-full-page="false" />
|
||||
<div class="lines" ref="lines">
|
||||
<template v-for="(l, i) in lines">
|
||||
<span :set="line = splitLine(l)" :key="i" class="line">
|
||||
<span class="timestamp" :title="line.file">{{ line.timestamp }}</span>
|
||||
<span class="log-message">{{ line.message }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Regexp for splitting log lines in the following format to
|
||||
// [timestamp] [file] [message].
|
||||
// 2021/05/01 00:00:00 init.go:99: reading config: config.toml
|
||||
const reFormatLine = new RegExp(/^([0-9\s:/]+) (.+?\.go:[0-9]+):\s/g);
|
||||
const reFormatLine = /^([0-9\s:/]+) (.+?\.go:[0-9]+):\s/g;
|
||||
|
||||
export default {
|
||||
name: 'LogView',
|
||||
|
|
|
@ -1,96 +1,64 @@
|
|||
<template>
|
||||
<b-menu-list>
|
||||
<b-menu-item :to="{name: 'dashboard'}" tag="router-link" :active="activeItem.dashboard"
|
||||
icon="view-dashboard-variant-outline" :label="$t('menu.dashboard')">
|
||||
</b-menu-item><!-- dashboard -->
|
||||
<b-menu-item :to="{ name: 'dashboard' }" tag="router-link" :active="activeItem.dashboard"
|
||||
icon="view-dashboard-variant-outline" :label="$t('menu.dashboard')" /><!-- dashboard -->
|
||||
|
||||
<b-menu-item :expanded="activeGroup.lists" :active="activeGroup.lists" data-cy="lists"
|
||||
v-on:update:active="(state) => toggleGroup('lists', state)" icon="format-list-bulleted-square"
|
||||
@update:active="(state) => toggleGroup('lists', state)" icon="format-list-bulleted-square"
|
||||
:label="$t('globals.terms.lists')">
|
||||
<b-menu-item :to="{name: 'lists'}" tag="router-link" :active="activeItem.lists"
|
||||
data-cy="all-lists" icon="format-list-bulleted-square" :label="$t('menu.allLists')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'forms'}" tag="router-link" :active="activeItem.forms"
|
||||
class="forms" icon="newspaper-variant-outline" :label="$t('menu.forms')">
|
||||
</b-menu-item>
|
||||
<b-menu-item :to="{ name: 'lists' }" tag="router-link" :active="activeItem.lists" data-cy="all-lists"
|
||||
icon="format-list-bulleted-square" :label="$t('menu.allLists')" />
|
||||
<b-menu-item :to="{ name: 'forms' }" tag="router-link" :active="activeItem.forms" class="forms"
|
||||
icon="newspaper-variant-outline" :label="$t('menu.forms')" />
|
||||
</b-menu-item><!-- lists -->
|
||||
|
||||
<b-menu-item :expanded="activeGroup.subscribers" :active="activeGroup.subscribers"
|
||||
data-cy="subscribers" v-on:update:active="(state) => toggleGroup('subscribers', state)"
|
||||
icon="account-multiple" :label="$t('globals.terms.subscribers')">
|
||||
|
||||
<b-menu-item :to="{name: 'subscribers'}" tag="router-link"
|
||||
:active="activeItem.subscribers" data-cy="all-subscribers" icon="account-multiple"
|
||||
:label="$t('menu.allSubscribers')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'import'}" tag="router-link" :active="activeItem.import"
|
||||
data-cy="import" icon="file-upload-outline" :label="$t('menu.import')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'bounces'}" tag="router-link" :active="activeItem.bounces"
|
||||
data-cy="bounces" icon="email-bounce" :label="$t('globals.terms.bounces')">
|
||||
</b-menu-item>
|
||||
<b-menu-item :expanded="activeGroup.subscribers" :active="activeGroup.subscribers" data-cy="subscribers"
|
||||
@update:active="(state) => toggleGroup('subscribers', state)" icon="account-multiple"
|
||||
:label="$t('globals.terms.subscribers')">
|
||||
<b-menu-item :to="{ name: 'subscribers' }" tag="router-link" :active="activeItem.subscribers"
|
||||
data-cy="all-subscribers" icon="account-multiple" :label="$t('menu.allSubscribers')" />
|
||||
<b-menu-item :to="{ name: 'import' }" tag="router-link" :active="activeItem.import" data-cy="import"
|
||||
icon="file-upload-outline" :label="$t('menu.import')" />
|
||||
<b-menu-item :to="{ name: 'bounces' }" tag="router-link" :active="activeItem.bounces" data-cy="bounces"
|
||||
icon="email-bounce" :label="$t('globals.terms.bounces')" />
|
||||
</b-menu-item><!-- subscribers -->
|
||||
|
||||
<b-menu-item :expanded="activeGroup.campaigns" :active="activeGroup.campaigns"
|
||||
data-cy="campaigns" v-on:update:active="(state) => toggleGroup('campaigns', state)"
|
||||
icon="rocket-launch-outline" :label="$t('globals.terms.campaigns')">
|
||||
|
||||
<b-menu-item :to="{name: 'campaigns'}" tag="router-link" :active="activeItem.campaigns"
|
||||
data-cy="all-campaigns" icon="rocket-launch-outline" :label="$t('menu.allCampaigns')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'campaign', params: {id: 'new'}}" tag="router-link"
|
||||
:active="activeItem.campaign" data-cy="new-campaign" icon="plus"
|
||||
:label="$t('menu.newCampaign')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'media'}" tag="router-link" :active="activeItem.media"
|
||||
data-cy="media" icon="image-outline" :label="$t('menu.media')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'templates'}" tag="router-link" :active="activeItem.templates"
|
||||
data-cy="templates" icon="file-image-outline" :label="$t('globals.terms.templates')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'campaignAnalytics'}" tag="router-link"
|
||||
:active="activeItem.campaignAnalytics" data-cy="analytics" icon="chart-bar"
|
||||
:label="$t('globals.terms.analytics')">
|
||||
</b-menu-item>
|
||||
<b-menu-item :expanded="activeGroup.campaigns" :active="activeGroup.campaigns" data-cy="campaigns"
|
||||
@update:active="(state) => toggleGroup('campaigns', state)" icon="rocket-launch-outline"
|
||||
:label="$t('globals.terms.campaigns')">
|
||||
<b-menu-item :to="{ name: 'campaigns' }" tag="router-link" :active="activeItem.campaigns" data-cy="all-campaigns"
|
||||
icon="rocket-launch-outline" :label="$t('menu.allCampaigns')" />
|
||||
<b-menu-item :to="{ name: 'campaign', params: { id: 'new' } }" tag="router-link" :active="activeItem.campaign"
|
||||
data-cy="new-campaign" icon="plus" :label="$t('menu.newCampaign')" />
|
||||
<b-menu-item :to="{ name: 'media' }" tag="router-link" :active="activeItem.media" data-cy="media"
|
||||
icon="image-outline" :label="$t('menu.media')" />
|
||||
<b-menu-item :to="{ name: 'templates' }" tag="router-link" :active="activeItem.templates" data-cy="templates"
|
||||
icon="file-image-outline" :label="$t('globals.terms.templates')" />
|
||||
<b-menu-item :to="{ name: 'campaignAnalytics' }" tag="router-link" :active="activeItem.campaignAnalytics"
|
||||
data-cy="analytics" icon="chart-bar" :label="$t('globals.terms.analytics')" />
|
||||
</b-menu-item><!-- campaigns -->
|
||||
|
||||
<b-menu-item :expanded="activeGroup.settings" :active="activeGroup.settings"
|
||||
data-cy="settings" v-on:update:active="(state) => toggleGroup('settings', state)"
|
||||
icon="cog-outline" :label="$t('menu.settings')">
|
||||
|
||||
<b-menu-item :to="{name: 'settings'}" tag="router-link" :active="activeItem.settings"
|
||||
data-cy="all-settings" icon="cog-outline" :label="$t('menu.settings')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'maintenance'}" tag="router-link" :active="activeItem.maintenance"
|
||||
data-cy="maintenance" icon="wrench-outline" :label="$t('menu.maintenance')">
|
||||
</b-menu-item>
|
||||
|
||||
<b-menu-item :to="{name: 'logs'}" tag="router-link" :active="activeItem.logs"
|
||||
data-cy="logs" icon="newspaper-variant-outline" :label="$t('menu.logs')">
|
||||
</b-menu-item>
|
||||
<b-menu-item :expanded="activeGroup.settings" :active="activeGroup.settings" data-cy="settings"
|
||||
@update:active="(state) => toggleGroup('settings', state)" icon="cog-outline" :label="$t('menu.settings')">
|
||||
<b-menu-item :to="{ name: 'settings' }" tag="router-link" :active="activeItem.settings" data-cy="all-settings"
|
||||
icon="cog-outline" :label="$t('menu.settings')" />
|
||||
<b-menu-item :to="{ name: 'maintenance' }" tag="router-link" :active="activeItem.maintenance" data-cy="maintenance"
|
||||
icon="wrench-outline" :label="$t('menu.maintenance')" />
|
||||
<b-menu-item :to="{ name: 'logs' }" tag="router-link" :active="activeItem.logs" data-cy="logs"
|
||||
icon="newspaper-variant-outline" :label="$t('menu.logs')" />
|
||||
</b-menu-item><!-- settings -->
|
||||
|
||||
<b-menu-item v-if="isMobile" icon="logout-variant" :label="$t('users.logout')"
|
||||
@click.prevent="doLogout">
|
||||
</b-menu-item>
|
||||
<b-menu-item v-if="isMobile" icon="logout-variant" :label="$t('users.logout')" @click.prevent="doLogout" />
|
||||
</b-menu-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'navigation',
|
||||
name: 'Navigation',
|
||||
|
||||
props: {
|
||||
activeItem: Object,
|
||||
activeGroup: Object,
|
||||
activeItem: { type: Object, default: () => { } },
|
||||
activeGroup: { type: Object, default: () => { } },
|
||||
isMobile: Boolean,
|
||||
},
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ export const models = Object.freeze({
|
|||
});
|
||||
|
||||
// Ad-hoc URIs that are used outside of vuex requests.
|
||||
const rootURL = process.env.VUE_APP_ROOT_URL || '/';
|
||||
const baseURL = process.env.BASE_URL.replace(/\/$/, '');
|
||||
const rootURL = import.meta.env.VUE_APP_ROOT_URL || '/';
|
||||
const baseURL = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
|
||||
export const uris = Object.freeze({
|
||||
previewCampaign: '/api/campaigns/:id/preview',
|
||||
|
|
|
@ -9,115 +9,115 @@ const routes = [
|
|||
path: '/404',
|
||||
name: '404_page',
|
||||
meta: { title: '404' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/404.vue'),
|
||||
component: () => import('../views/404.vue'),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'dashboard',
|
||||
meta: { title: '' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Dashboard.vue'),
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
},
|
||||
{
|
||||
path: '/lists',
|
||||
name: 'lists',
|
||||
meta: { title: 'globals.terms.lists', group: 'lists' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Lists.vue'),
|
||||
component: () => import('../views/Lists.vue'),
|
||||
},
|
||||
{
|
||||
path: '/lists/forms',
|
||||
name: 'forms',
|
||||
meta: { title: 'forms.title', group: 'lists' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Forms.vue'),
|
||||
component: () => import('../views/Forms.vue'),
|
||||
},
|
||||
{
|
||||
path: '/lists/:id',
|
||||
name: 'list',
|
||||
meta: { title: 'globals.terms.lists', group: 'lists' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Lists.vue'),
|
||||
component: () => import('../views/Lists.vue'),
|
||||
},
|
||||
{
|
||||
path: '/subscribers',
|
||||
name: 'subscribers',
|
||||
meta: { title: 'globals.terms.subscribers', group: 'subscribers' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Subscribers.vue'),
|
||||
component: () => import('../views/Subscribers.vue'),
|
||||
},
|
||||
{
|
||||
path: '/subscribers/import',
|
||||
name: 'import',
|
||||
meta: { title: 'import.title', group: 'subscribers' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Import.vue'),
|
||||
component: () => import('../views/Import.vue'),
|
||||
},
|
||||
{
|
||||
path: '/subscribers/bounces',
|
||||
name: 'bounces',
|
||||
meta: { title: 'globals.terms.bounces', group: 'subscribers' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Bounces.vue'),
|
||||
component: () => import('../views/Bounces.vue'),
|
||||
},
|
||||
{
|
||||
path: '/subscribers/lists/:listID',
|
||||
name: 'subscribers_list',
|
||||
meta: { title: 'globals.terms.subscribers', group: 'subscribers' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Subscribers.vue'),
|
||||
component: () => import('../views/Subscribers.vue'),
|
||||
},
|
||||
{
|
||||
path: '/subscribers/:id',
|
||||
name: 'subscriber',
|
||||
meta: { title: 'globals.terms.subscribers', group: 'subscribers' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Subscribers.vue'),
|
||||
component: () => import('../views/Subscribers.vue'),
|
||||
},
|
||||
{
|
||||
path: '/campaigns',
|
||||
name: 'campaigns',
|
||||
meta: { title: 'globals.terms.campaigns', group: 'campaigns' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Campaigns.vue'),
|
||||
component: () => import('../views/Campaigns.vue'),
|
||||
},
|
||||
{
|
||||
path: '/campaigns/media',
|
||||
name: 'media',
|
||||
meta: { title: 'globals.terms.media', group: 'campaigns' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Media.vue'),
|
||||
component: () => import('../views/Media.vue'),
|
||||
},
|
||||
{
|
||||
path: '/campaigns/templates',
|
||||
name: 'templates',
|
||||
meta: { title: 'globals.terms.templates', group: 'campaigns' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Templates.vue'),
|
||||
component: () => import('../views/Templates.vue'),
|
||||
},
|
||||
{
|
||||
path: '/campaigns/analytics',
|
||||
name: 'campaignAnalytics',
|
||||
meta: { title: 'analytics.title', group: 'campaigns' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/CampaignAnalytics.vue'),
|
||||
component: () => import('../views/CampaignAnalytics.vue'),
|
||||
},
|
||||
{
|
||||
path: '/campaigns/:id',
|
||||
name: 'campaign',
|
||||
meta: { title: 'globals.terms.campaign', group: 'campaigns' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Campaign.vue'),
|
||||
component: () => import('../views/Campaign.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
meta: { title: 'globals.terms.settings', group: 'settings' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Settings.vue'),
|
||||
component: () => import('../views/Settings.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/logs',
|
||||
name: 'logs',
|
||||
meta: { title: 'logs.title', group: 'settings' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Logs.vue'),
|
||||
component: () => import('../views/Logs.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/maintenance',
|
||||
name: 'maintenance',
|
||||
meta: { title: 'maintenance.title', group: 'settings' },
|
||||
component: () => import(/* webpackChunkName: "main" */ '../views/Maintenance.vue'),
|
||||
component: () => import('../views/Maintenance.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
base: import.meta.env.BASE_URL,
|
||||
routes,
|
||||
|
||||
scrollBehavior(to) {
|
||||
|
|
|
@ -101,7 +101,7 @@ export default class Utils {
|
|||
}
|
||||
|
||||
return out.toFixed(2) + pfx;
|
||||
}
|
||||
};
|
||||
|
||||
formatNumber(v) {
|
||||
return this.intlNumFormat.format(v);
|
||||
|
@ -122,7 +122,7 @@ export default class Utils {
|
|||
}
|
||||
|
||||
return ids.map((id) => parseInt(id, 10));
|
||||
}
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/12034334
|
||||
escapeHTML = (html) => html.replace(/[&<>"'`=/]/g, (s) => htmlEntities[s]);
|
||||
|
@ -178,7 +178,7 @@ export default class Utils {
|
|||
camelString = (str) => {
|
||||
const s = str.replace(/[-_\s]+(.)?/g, (match, chr) => (chr ? chr.toUpperCase() : ''));
|
||||
return s.substr(0, 1).toLowerCase() + s.substr(1);
|
||||
}
|
||||
};
|
||||
|
||||
// camelKeys recursively camelCases all keys in a given object (array or {}).
|
||||
// For each key it traverses, it passes a dot separated key path to an optional testFunc() bool.
|
||||
|
@ -233,5 +233,5 @@ export default class Utils {
|
|||
|
||||
p[key] = val;
|
||||
localStorage.setItem(prefKey, JSON.stringify(p));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<section class="page-404">
|
||||
<h1 class="title">404</h1>
|
||||
<h1 class="title">
|
||||
404
|
||||
</h1>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
<section class="bounces">
|
||||
<header class="page-header columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title is-4">{{ $t('globals.terms.bounces') }}
|
||||
<span v-if="bounces.total > 0">({{ bounces.total }})</span></h1>
|
||||
<h1 class="title is-4">
|
||||
{{ $t('globals.terms.bounces') }}
|
||||
<span v-if="bounces.total > 0">({{ bounces.total }})</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column has-text-right buttons">
|
||||
<b-button v-if="bulk.checked.length > 0 || bulk.all" type="is-primary"
|
||||
icon-left="trash-can-outline" data-cy="btn-delete"
|
||||
@click.prevent="$utils.confirm(null, () => deleteBounces())">
|
||||
<b-button v-if="bulk.checked.length > 0 || bulk.all" type="is-primary" icon-left="trash-can-outline"
|
||||
data-cy="btn-delete" @click.prevent="$utils.confirm(null, () => deleteBounces())">
|
||||
{{ $t('globals.buttons.clear') }}
|
||||
</b-button>
|
||||
<b-button v-if="bounces.total" icon-left="trash-can-outline" data-cy="btn-delete"
|
||||
|
@ -18,28 +19,19 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<b-table :data="bounces.results" :hoverable="true" :loading="loading.bounces"
|
||||
default-sort="createdAt"
|
||||
checkable
|
||||
@check-all="onTableCheck" @check="onTableCheck"
|
||||
:checked-rows.sync="bulk.checked"
|
||||
detailed
|
||||
show-detail-icon
|
||||
@details-open="(row) => $buefy.toast.open(`Expanded ${row.user.first_name}`)"
|
||||
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
||||
:current-page="queryParams.page" :per-page="bounces.perPage" :total="bounces.total"
|
||||
backend-sorting @sort="onSort">
|
||||
<b-table-column v-slot="props" field="email" :label="$t('subscribers.email')"
|
||||
:td-attrs="$utils.tdID" sortable>
|
||||
<router-link :to="{ name: 'subscriber', params: { id: props.row.subscriberId }}">
|
||||
<b-table :data="bounces.results" :hoverable="true" :loading="loading.bounces" default-sort="createdAt" checkable
|
||||
@check-all="onTableCheck" @check="onTableCheck" :checked-rows.sync="bulk.checked" detailed show-detail-icon
|
||||
@details-open="(row) => $buefy.toast.open(`Expanded ${row.user.first_name}`)" paginated backend-pagination
|
||||
pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page" :per-page="bounces.perPage"
|
||||
:total="bounces.total" backend-sorting @sort="onSort">
|
||||
<b-table-column v-slot="props" field="email" :label="$t('subscribers.email')" :td-attrs="$utils.tdID" sortable>
|
||||
<router-link :to="{ name: 'subscriber', params: { id: props.row.subscriberId } }">
|
||||
{{ props.row.email }}
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="campaign" :label="$tc('globals.terms.campaign')"
|
||||
sortable>
|
||||
<router-link v-if="props.row.campaign"
|
||||
:to="{ name: 'bounces', query: { campaign_id: props.row.campaign.id }}">
|
||||
<b-table-column v-slot="props" field="campaign" :label="$tc('globals.terms.campaign')" sortable>
|
||||
<router-link v-if="props.row.campaign" :to="{ name: 'bounces', query: { campaign_id: props.row.campaign.id } }">
|
||||
{{ props.row.campaign.name }}
|
||||
</router-link>
|
||||
<span v-else>-</span>
|
||||
|
@ -57,22 +49,20 @@
|
|||
</router-link>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="created_at"
|
||||
:label="$t('globals.fields.createdAt')" sortable>
|
||||
<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')" sortable>
|
||||
{{ $utils.niceDate(props.row.createdAt, true) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="actions" align="right">
|
||||
<div>
|
||||
<a v-if="!props.row.isDefault" href="#"
|
||||
@click.prevent="$utils.confirm(null, () => deleteBounce(props.row))"
|
||||
data-cy="btn-delete">
|
||||
<a v-if="!props.row.isDefault" href="#" @click.prevent="$utils.confirm(null, () => deleteBounce(props.row))"
|
||||
data-cy="btn-delete" :aria-label="$t('globals.buttons.delete')">
|
||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<span v-else class="a has-text-grey-light">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</span>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
@ -163,8 +153,10 @@ export default Vue.extend({
|
|||
deleteBounces(all) {
|
||||
const fnSuccess = () => {
|
||||
this.getBounces();
|
||||
this.$utils.toast(this.$t('globals.messages.deletedCount',
|
||||
{ name: this.$tc('globals.terms.bounces'), num: this.bounces.total }));
|
||||
this.$utils.toast(this.$t(
|
||||
'globals.messages.deletedCount',
|
||||
{ name: this.$tc('globals.terms.bounces'), num: this.bounces.total },
|
||||
));
|
||||
};
|
||||
|
||||
if (all) {
|
||||
|
|
|
@ -14,29 +14,32 @@
|
|||
{{ $t('globals.fields.uuid') }}: {{ data.uuid }}
|
||||
</span>
|
||||
</p>
|
||||
<h4 v-if="isEditing" class="title is-4">{{ data.name }}</h4>
|
||||
<h4 v-else class="title is-4">{{ $t('campaigns.newCampaign') }}</h4>
|
||||
<h4 v-if="isEditing" class="title is-4">
|
||||
{{ data.name }}
|
||||
</h4>
|
||||
<h4 v-else class="title is-4">
|
||||
{{ $t('campaigns.newCampaign') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="buttons">
|
||||
<b-field grouped v-if="isEditing && canEdit">
|
||||
<b-field expanded>
|
||||
<b-button expanded @click="() => onSubmit('update')" :loading="loading.campaigns"
|
||||
type="is-primary" icon-left="content-save-outline" data-cy="btn-save">
|
||||
<b-button expanded @click="() => onSubmit('update')" :loading="loading.campaigns" type="is-primary"
|
||||
icon-left="content-save-outline" data-cy="btn-save">
|
||||
{{ $t('globals.buttons.saveChanges') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
<b-field expanded v-if="canStart">
|
||||
<b-button expanded @click="startCampaign" :loading="loading.campaigns"
|
||||
type="is-primary" icon-left="rocket-launch-outline" data-cy="btn-start">
|
||||
<b-button expanded @click="startCampaign" :loading="loading.campaigns" type="is-primary"
|
||||
icon-left="rocket-launch-outline" data-cy="btn-start">
|
||||
{{ $t('campaigns.start') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
<b-field expanded v-if="canSchedule">
|
||||
<b-button expanded @click="startCampaign"
|
||||
:loading="loading.campaigns"
|
||||
type="is-primary" icon-left="clock-start" data-cy="btn-schedule">
|
||||
<b-button expanded @click="startCampaign" :loading="loading.campaigns" type="is-primary"
|
||||
icon-left="clock-start" data-cy="btn-schedule">
|
||||
{{ $t('campaigns.schedule') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
|
@ -45,85 +48,72 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<b-loading :active="loading.campaigns"></b-loading>
|
||||
<b-loading :active="loading.campaigns" />
|
||||
|
||||
<b-tabs type="is-boxed" :animated="false" v-model="activeTab" @input="onTab">
|
||||
<b-tab-item :label="$tc('globals.terms.campaign')" label-position="on-border"
|
||||
value="campaign" icon="rocket-launch-outline">
|
||||
<b-tab-item :label="$tc('globals.terms.campaign')" label-position="on-border" value="campaign"
|
||||
icon="rocket-launch-outline">
|
||||
<section class="wrap">
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<form @submit.prevent="() => onSubmit(isNew ? 'create' : 'update')">
|
||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
||||
name="name" :disabled="!canEdit"
|
||||
:placeholder="$t('globals.fields.name')" required></b-input>
|
||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" name="name" :disabled="!canEdit"
|
||||
:placeholder="$t('globals.fields.name')" required />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('campaigns.subject')" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.subject"
|
||||
name="subject" :disabled="!canEdit"
|
||||
:placeholder="$t('campaigns.subject')" required></b-input>
|
||||
<b-input :maxlength="200" v-model="form.subject" name="subject" :disabled="!canEdit"
|
||||
:placeholder="$t('campaigns.subject')" required />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('campaigns.fromAddress')" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.fromEmail"
|
||||
name="from_email" :disabled="!canEdit"
|
||||
:placeholder="$t('campaigns.fromAddressPlaceholder')" required></b-input>
|
||||
<b-input :maxlength="200" v-model="form.fromEmail" name="from_email" :disabled="!canEdit"
|
||||
:placeholder="$t('campaigns.fromAddressPlaceholder')" required />
|
||||
</b-field>
|
||||
|
||||
<list-selector
|
||||
v-model="form.lists"
|
||||
:selected="form.lists"
|
||||
:all="lists.results"
|
||||
:disabled="!canEdit"
|
||||
:label="$t('globals.terms.lists')"
|
||||
:placeholder="$t('campaigns.sendToLists')"
|
||||
></list-selector>
|
||||
<list-selector v-model="form.lists" :selected="form.lists" :all="lists.results" :disabled="!canEdit"
|
||||
:label="$t('globals.terms.lists')" :placeholder="$t('campaigns.sendToLists')" />
|
||||
|
||||
<b-field :label="$tc('globals.terms.template')" label-position="on-border">
|
||||
<b-select :placeholder="$tc('globals.terms.template')" v-model="form.templateId"
|
||||
name="template" :disabled="!canEdit" required>
|
||||
<b-select :placeholder="$tc('globals.terms.template')" v-model="form.templateId" name="template"
|
||||
:disabled="!canEdit" required>
|
||||
<template v-for="t in templates">
|
||||
<option v-if="t.type === 'campaign'"
|
||||
:value="t.id" :key="t.id">{{ t.name }}</option>
|
||||
<option v-if="t.type === 'campaign'" :value="t.id" :key="t.id">
|
||||
{{ t.name }}
|
||||
</option>
|
||||
</template>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$tc('globals.terms.messenger')" label-position="on-border">
|
||||
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger"
|
||||
name="messenger" :disabled="!canEdit" required>
|
||||
<option v-for="m in messengers"
|
||||
:value="m" :key="m">{{ m }}</option>
|
||||
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger" name="messenger"
|
||||
:disabled="!canEdit" required>
|
||||
<option v-for="m in messengers" :value="m" :key="m">
|
||||
{{ m }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('globals.terms.tags')" label-position="on-border">
|
||||
<b-taginput v-model="form.tags" name="tags" :disabled="!canEdit"
|
||||
ellipsis icon="tag-outline" :placeholder="$t('globals.terms.tags')" />
|
||||
<b-taginput v-model="form.tags" name="tags" :disabled="!canEdit" ellipsis icon="tag-outline"
|
||||
:placeholder="$t('globals.terms.tags')" />
|
||||
</b-field>
|
||||
<hr />
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('campaigns.sendLater')" data-cy="btn-send-later">
|
||||
<b-switch v-model="form.sendLater" :disabled="!canEdit" />
|
||||
<b-switch v-model="form.sendLater" :disabled="!canEdit" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<br />
|
||||
<b-field v-if="form.sendLater" data-cy="send_at"
|
||||
:message="form.sendAtDate ? $utils.duration(Date(), form.sendAtDate) : ''">
|
||||
<b-datetimepicker
|
||||
v-model="form.sendAtDate"
|
||||
:disabled="!canEdit"
|
||||
:placeholder="$t('campaigns.dateAndTime')"
|
||||
icon="calendar-clock"
|
||||
:timepicker="{ hourFormat: '24' }"
|
||||
:datetime-formatter="formatDateTime"
|
||||
horizontal-time-picker>
|
||||
</b-datetimepicker>
|
||||
<b-datetimepicker v-model="form.sendAtDate" :disabled="!canEdit"
|
||||
:placeholder="$t('campaigns.dateAndTime')" icon="calendar-clock"
|
||||
:timepicker="{ hourFormat: '24' }" :datetime-formatter="formatDateTime" horizontal-time-picker />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -134,18 +124,17 @@
|
|||
<b-icon icon="plus" />{{ $t('settings.smtp.setCustomHeaders') }}
|
||||
</a>
|
||||
</p>
|
||||
<b-field v-if="form.headersStr !== '[]' || isHeadersVisible"
|
||||
label-position="on-border" :message="$t('campaigns.customHeadersHelp')">
|
||||
<b-field v-if="form.headersStr !== '[]' || isHeadersVisible" label-position="on-border"
|
||||
:message="$t('campaigns.customHeadersHelp')">
|
||||
<b-input v-model="form.headersStr" name="headers" type="textarea"
|
||||
placeholder='[{"X-Custom": "value"}, {"X-Custom2": "value"}]'
|
||||
placeholder="[{"X-Custom": "value"}, {"X-Custom2": "value"}]"
|
||||
:disabled="!canEdit" />
|
||||
</b-field>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<b-field v-if="isNew">
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
:loading="loading.campaigns" data-cy="btn-continue">
|
||||
<b-button native-type="submit" type="is-primary" :loading="loading.campaigns" data-cy="btn-continue">
|
||||
{{ $t('campaigns.continue') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
|
@ -154,18 +143,19 @@
|
|||
<div class="column is-4 is-offset-1">
|
||||
<br />
|
||||
<div class="box">
|
||||
<h3 class="title is-size-6">{{ $t('campaigns.sendTest') }}</h3>
|
||||
<b-field :message="$t('campaigns.sendTestHelp')">
|
||||
<b-taginput v-model="form.testEmails"
|
||||
:before-adding="$utils.validateEmail" :disabled="isNew"
|
||||
ellipsis icon="email-outline" :placeholder="$t('campaigns.testEmails')" />
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-button @click="() => onSubmit('test')" :loading="loading.campaigns"
|
||||
:disabled="isNew" type="is-primary" icon-left="email-outline">
|
||||
{{ $t('campaigns.send') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
<h3 class="title is-size-6">
|
||||
{{ $t('campaigns.sendTest') }}
|
||||
</h3>
|
||||
<b-field :message="$t('campaigns.sendTestHelp')">
|
||||
<b-taginput v-model="form.testEmails" :before-adding="$utils.validateEmail" :disabled="isNew" ellipsis
|
||||
icon="email-outline" :placeholder="$t('campaigns.testEmails')" />
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-button @click="() => onSubmit('test')" :loading="loading.campaigns" :disabled="isNew"
|
||||
type="is-primary" icon-left="email-outline">
|
||||
{{ $t('campaigns.send') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -173,34 +163,26 @@
|
|||
</b-tab-item><!-- campaign -->
|
||||
|
||||
<b-tab-item :label="$t('campaigns.content')" icon="text" :disabled="isNew" value="content">
|
||||
<editor
|
||||
v-model="form.content"
|
||||
:id="data.id"
|
||||
:title="data.name"
|
||||
:templateId="form.templateId"
|
||||
:contentType="data.contentType"
|
||||
:body="data.body"
|
||||
:disabled="!canEdit"
|
||||
/>
|
||||
<editor v-model="form.content" :id="data.id" :title="data.name" :template-id="form.templateId"
|
||||
:content-type="data.contentType" :body="data.body" :disabled="!canEdit" />
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<p v-if="!isAttachFieldVisible" class="is-size-6 has-text-grey">
|
||||
<a href="#" @click.prevent="onShowAttachField()" data-cy="btn-attach">
|
||||
<b-icon icon="file-upload-outline" size="is-small" />
|
||||
{{ $t('campaigns.addAttachments') }}
|
||||
{{ $t('campaigns.addAttachments') }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<b-field v-if="isAttachFieldVisible" :label="$t('campaigns.attachments')"
|
||||
label-position="on-border" expanded data-cy="media">
|
||||
<b-taginput v-model="form.media" name="media" ellipsis icon="tag-outline"
|
||||
ref="media" field="filename" @focus="onOpenAttach" :disabled="!canEdit" />
|
||||
<b-field v-if="isAttachFieldVisible" :label="$t('campaigns.attachments')" label-position="on-border" expanded
|
||||
data-cy="media">
|
||||
<b-taginput v-model="form.media" name="media" ellipsis icon="tag-outline" ref="media" field="filename"
|
||||
@focus="onOpenAttach" :disabled="!canEdit" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<p v-if="canEdit && form.content.contentType !== 'plain'"
|
||||
class="is-size-6 has-text-grey">
|
||||
<p v-if="canEdit && form.content.contentType !== 'plain'" class="is-size-6 has-text-grey">
|
||||
<a v-if="form.altbody === null" href="#" @click.prevent="onAddAltBody">
|
||||
<b-icon icon="text" size="is-small" /> {{ $t('campaigns.addAltText') }}
|
||||
</a>
|
||||
|
@ -213,23 +195,20 @@
|
|||
</div>
|
||||
|
||||
<div v-if="canEdit && form.content.contentType !== 'plain'" class="alt-body">
|
||||
<b-input v-if="form.altbody !== null" v-model="form.altbody"
|
||||
type="textarea" :disabled="!canEdit" />
|
||||
<b-input v-if="form.altbody !== null" v-model="form.altbody" type="textarea" :disabled="!canEdit" />
|
||||
</div>
|
||||
</b-tab-item><!-- content -->
|
||||
|
||||
<b-tab-item :label="$t('campaigns.archive')" icon="newspaper-variant-outline"
|
||||
value="archive" :disabled="isNew">
|
||||
<b-tab-item :label="$t('campaigns.archive')" icon="newspaper-variant-outline" value="archive" :disabled="isNew">
|
||||
<section class="wrap">
|
||||
<b-field :label="$t('campaigns.archiveEnable')" data-cy="btn-archive"
|
||||
:message="$t('campaigns.archiveHelp')">
|
||||
<b-field :label="$t('campaigns.archiveEnable')" data-cy="btn-archive" :message="$t('campaigns.archiveHelp')">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-switch data-cy="btn-archive" v-model="form.archive" :disabled="!canArchive" />
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<a :href="`${settings['app.root_url']}/archive/${data.uuid}`" target="_blank"
|
||||
:class="{'has-text-grey-light': !form.archive}">
|
||||
<a :href="`${settings['app.root_url']}/archive/${data.uuid}`" target="_blank" rel="noopener noreferer"
|
||||
:class="{ 'has-text-grey-light': !form.archive }" aria-label="$t('campaigns.archive')">
|
||||
<b-icon icon="link-variant" />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -239,31 +218,31 @@
|
|||
<div class="columns">
|
||||
<div class="column is-8">
|
||||
<b-field :label="$tc('globals.terms.template')" label-position="on-border">
|
||||
<b-select :placeholder="$tc('globals.terms.template')"
|
||||
v-model="form.archiveTemplateId" name="template"
|
||||
<b-select :placeholder="$tc('globals.terms.template')" v-model="form.archiveTemplateId" name="template"
|
||||
:disabled="!canArchive || !form.archive" required>
|
||||
<template v-for="t in templates">
|
||||
<option v-if="t.type === 'campaign'"
|
||||
:value="t.id" :key="t.id">{{ t.name }}</option>
|
||||
<option v-if="t.type === 'campaign'" :value="t.id" :key="t.id">
|
||||
{{ t.name }}
|
||||
</option>
|
||||
</template>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div class="column has-text-right">
|
||||
<a v-if="!this.form.archiveMetaStr || this.form.archiveMetaStr === '{}'"
|
||||
class="button" href="#" @click.prevent="onFillArchiveMeta">{}</a>
|
||||
<a v-if="!this.form.archiveMetaStr || this.form.archiveMetaStr === '{}'" class="button" href="#"
|
||||
@click.prevent="onFillArchiveMeta">{}</a>
|
||||
</div>
|
||||
</div>
|
||||
<b-field :label="$t('campaigns.archiveMeta')"
|
||||
:message="$t('campaigns.archiveMetaHelp')" label-position="on-border">
|
||||
<b-input v-model="form.archiveMetaStr" name="archive_meta" type="textarea"
|
||||
data-cy="archive-meta" :disabled="!canArchive || !form.archive" rows="20" />
|
||||
<b-field :label="$t('campaigns.archiveMeta')" :message="$t('campaigns.archiveMetaHelp')"
|
||||
label-position="on-border">
|
||||
<b-input v-model="form.archiveMetaStr" name="archive_meta" type="textarea" data-cy="archive-meta"
|
||||
:disabled="!canArchive || !form.archive" rows="20" />
|
||||
</b-field>
|
||||
|
||||
<b-field v-if="!canEdit && canArchive">
|
||||
<b-button @click="onUpdateCampaignArchive" :loading="loading.campaigns"
|
||||
type="is-primary" icon-left="content-save-outline" data-cy="btn-archive-save">
|
||||
<b-button @click="onUpdateCampaignArchive" :loading="loading.campaigns" type="is-primary"
|
||||
icon-left="content-save-outline" data-cy="btn-archive-save">
|
||||
{{ $t('globals.buttons.saveChanges') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
|
@ -282,13 +261,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import dayjs from 'dayjs';
|
||||
import htmlToPlainText from 'textversionjs';
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
import ListSelector from '../components/ListSelector.vue';
|
||||
import Editor from '../components/Editor.vue';
|
||||
import ListSelector from '../components/ListSelector.vue';
|
||||
import Media from './Media.vue';
|
||||
|
||||
const TABS = ['campaign', 'content', 'archive'];
|
||||
|
@ -568,7 +547,8 @@ export default Vue.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
this.$utils.confirm(null,
|
||||
this.$utils.confirm(
|
||||
null,
|
||||
() => {
|
||||
// First save the campaign.
|
||||
this.updateCampaign().then(() => {
|
||||
|
@ -586,7 +566,8 @@ export default Vue.extend({
|
|||
this.$router.push({ name: 'campaigns' });
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<template>
|
||||
<section class="analytics content relative">
|
||||
<h1 class="title is-4">{{ $t('analytics.title') }}</h1>
|
||||
<h1 class="title is-4">
|
||||
{{ $t('analytics.title') }}
|
||||
</h1>
|
||||
<hr />
|
||||
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('globals.terms.campaigns')" label-position="on-border">
|
||||
<b-taginput v-model="form.campaigns" :data="queriedCampaigns" name="campaigns" ellipsis
|
||||
icon="tag-outline" :placeholder="$t('globals.terms.campaigns')"
|
||||
autocomplete :allow-new="false" :before-adding="isCampaignSelected"
|
||||
@typing="queryCampaigns" field="name" :loading="isSearchLoading"></b-taginput>
|
||||
<b-taginput v-model="form.campaigns" :data="queriedCampaigns" name="campaigns" ellipsis icon="tag-outline"
|
||||
:placeholder="$t('globals.terms.campaigns')" autocomplete :allow-new="false"
|
||||
:before-adding="isCampaignSelected" @typing="queryCampaigns" field="name" :loading="isSearchLoading" />
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
|
@ -18,19 +19,13 @@
|
|||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field data-cy="from" :label="$t('analytics.fromDate')" label-position="on-border">
|
||||
<b-datetimepicker
|
||||
v-model="form.from"
|
||||
icon="calendar-clock"
|
||||
:timepicker="{ hourFormat: '24' }"
|
||||
<b-datetimepicker v-model="form.from" icon="calendar-clock" :timepicker="{ hourFormat: '24' }"
|
||||
:datetime-formatter="formatDateTime" @input="onFromDateChange" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<b-field data-cy="to" :label="$t('analytics.toDate')" label-position="on-border">
|
||||
<b-datetimepicker
|
||||
v-model="form.to"
|
||||
icon="calendar-clock"
|
||||
:timepicker="{ hourFormat: '24' }"
|
||||
<b-datetimepicker v-model="form.to" icon="calendar-clock" :timepicker="{ hourFormat: '24' }"
|
||||
:datetime-formatter="formatDateTime" @input="onToDateChange" />
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -38,8 +33,8 @@
|
|||
</div><!-- columns -->
|
||||
|
||||
<div class="column is-1">
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify"
|
||||
:disabled="form.campaigns.length === 0" data-cy="btn-search"></b-button>
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify" :disabled="form.campaigns.length === 0"
|
||||
data-cy="btn-search" />
|
||||
</div>
|
||||
</div><!-- columns -->
|
||||
</form>
|
||||
|
@ -48,7 +43,9 @@
|
|||
<template v-if="settings['privacy.individual_tracking']">
|
||||
{{ $t('analytics.isUnique') }}
|
||||
</template>
|
||||
<template v-else>{{ $t('analytics.nonUnique') }}</template>
|
||||
<template v-else>
|
||||
{{ $t('analytics.nonUnique') }}
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<section class="charts mt-5">
|
||||
|
@ -59,10 +56,10 @@
|
|||
{{ v.name }}
|
||||
<span class="has-text-grey-light">({{ $utils.niceNumber(counts[k]) }})</span>
|
||||
</h4>
|
||||
<div :ref="`chart-${k}`" :id="`chart-${k}`"></div>
|
||||
<div :ref="`chart-${k}`" :id="`chart-${k}`" />
|
||||
</div>
|
||||
<div class="column is-2 donut-container">
|
||||
<div :ref="`donut-${k}`" :id="`donut-${k}`" class="donut"></div>
|
||||
<div :ref="`donut-${k}`" :id="`donut-${k}`" class="donut" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -70,14 +67,14 @@
|
|||
</template>
|
||||
|
||||
<style lang="css">
|
||||
@import "~c3/c3.css";
|
||||
@import "c3/c3.css";
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import c3 from 'c3';
|
||||
import dayjs from 'dayjs';
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import dayjs from 'dayjs';
|
||||
import c3 from 'c3';
|
||||
import { colors } from '../constants';
|
||||
|
||||
const chartColorRed = '#ee7d5b';
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
<section class="campaigns">
|
||||
<header class="columns page-header">
|
||||
<div class="column is-10">
|
||||
<h1 class="title is-4">{{ $t('globals.terms.campaigns') }}
|
||||
<h1 class="title is-4">
|
||||
{{ $t('globals.terms.campaigns') }}
|
||||
<span v-if="!isNaN(campaigns.total)">({{ campaigns.total }})</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<b-field expanded>
|
||||
<b-button expanded :to="{name: 'campaign', params:{id: 'new'}}"
|
||||
tag="router-link" class="btn-new"
|
||||
<b-button expanded :to="{ name: 'campaign', params: { id: 'new' } }" tag="router-link" class="btn-new"
|
||||
type="is-primary" icon-left="plus" data-cy="btn-new">
|
||||
{{ $t('globals.buttons.new') }}
|
||||
</b-button>
|
||||
|
@ -17,14 +17,9 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<b-table
|
||||
:data="campaigns.results"
|
||||
:loading="loading.campaigns"
|
||||
:row-class="highlightedRow"
|
||||
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
||||
:current-page="queryParams.page" :per-page="campaigns.perPage" :total="campaigns.total"
|
||||
hoverable backend-sorting @sort="onSort">
|
||||
|
||||
<b-table :data="campaigns.results" :loading="loading.campaigns" :row-class="highlightedRow" paginated
|
||||
backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
|
||||
:per-page="campaigns.perPage" :total="campaigns.total" hoverable backend-sorting @sort="onSort">
|
||||
<template #top-left>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
|
@ -43,12 +38,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="status" field="status"
|
||||
:label="$t('globals.fields.status')" width="10%" sortable
|
||||
:td-attrs="$utils.tdID" header-class="cy-status">
|
||||
<b-table-column v-slot="props" cell-class="status" field="status" :label="$t('globals.fields.status')" width="10%"
|
||||
sortable :td-attrs="$utils.tdID" header-class="cy-status">
|
||||
<div>
|
||||
<p>
|
||||
<router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
|
||||
<router-link :to="{ name: 'campaign', params: { id: props.row.id } }">
|
||||
<b-tag :class="props.row.status">
|
||||
{{ $t(`campaigns.status.${props.row.status}`) }}
|
||||
</b-tag>
|
||||
|
@ -71,50 +65,53 @@
|
|||
</p>
|
||||
</div>
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" width="25%"
|
||||
sortable header-class="cy-name">
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" width="25%" sortable
|
||||
header-class="cy-name">
|
||||
<div>
|
||||
<p>
|
||||
<b-tag v-if="props.row.type === 'optin'" class="is-small">
|
||||
{{ $t('lists.optin') }}
|
||||
</b-tag>
|
||||
<router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
|
||||
{{ props.row.name }}</router-link>
|
||||
<router-link :to="{ name: 'campaign', params: { id: props.row.id } }">
|
||||
{{ props.row.name }}
|
||||
</router-link>
|
||||
</p>
|
||||
<p class="is-size-7 has-text-grey">
|
||||
{{ props.row.subject }}
|
||||
</p>
|
||||
<p class="is-size-7 has-text-grey">{{ props.row.subject }}</p>
|
||||
<b-taglist>
|
||||
<b-tag class="is-small" v-for="t in props.row.tags" :key="t">{{ t }}</b-tag>
|
||||
<b-tag class="is-small" v-for="t in props.row.tags" :key="t">
|
||||
{{ t }}
|
||||
</b-tag>
|
||||
</b-taglist>
|
||||
</div>
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" cell-class="lists" field="lists"
|
||||
:label="$t('globals.terms.lists')" width="15%">
|
||||
<b-table-column v-slot="props" cell-class="lists" field="lists" :label="$t('globals.terms.lists')" width="15%">
|
||||
<ul>
|
||||
<li v-for="l in props.row.lists" :key="l.id">
|
||||
<router-link :to="{name: 'subscribers_list', params: { listID: l.id }}">
|
||||
<router-link :to="{ name: 'subscribers_list', params: { listID: l.id } }">
|
||||
{{ l.name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="created_at" :label="$t('campaigns.timestamps')"
|
||||
width="19%" sortable header-class="cy-timestamp">
|
||||
<b-table-column v-slot="props" field="created_at" :label="$t('campaigns.timestamps')" width="19%" sortable
|
||||
header-class="cy-timestamp">
|
||||
<div class="fields timestamps" :set="stats = getCampaignStats(props.row)">
|
||||
<p>
|
||||
<label>{{ $t('globals.fields.createdAt') }}</label>
|
||||
<label for="#">{{ $t('globals.fields.createdAt') }}</label>
|
||||
<span>{{ $utils.niceDate(props.row.createdAt, true) }}</span>
|
||||
</p>
|
||||
<p v-if="stats.startedAt">
|
||||
<label>{{ $t('campaigns.startedAt') }}</label>
|
||||
<label for="#">{{ $t('campaigns.startedAt') }}</label>
|
||||
<span>{{ $utils.niceDate(stats.startedAt, true) }}</span>
|
||||
</p>
|
||||
<p v-if="isDone(props.row)">
|
||||
<label>{{ $t('campaigns.ended') }}</label>
|
||||
<label for="#">{{ $t('campaigns.ended') }}</label>
|
||||
<span>{{ $utils.niceDate(stats.updatedAt, true) }}</span>
|
||||
</p>
|
||||
<p v-if="stats.startedAt && stats.updatedAt"
|
||||
class="is-capitalized">
|
||||
<label><b-icon icon="alarm" size="is-small" /></label>
|
||||
<p v-if="stats.startedAt && stats.updatedAt" class="is-capitalized">
|
||||
<label for="#"><b-icon icon="alarm" size="is-small" /></label>
|
||||
<span>{{ $utils.duration(stats.startedAt, stats.updatedAt) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -123,41 +120,40 @@
|
|||
<b-table-column v-slot="props" field="stats" :label="$t('campaigns.stats')" width="15%">
|
||||
<div class="fields stats" :set="stats = getCampaignStats(props.row)">
|
||||
<p>
|
||||
<label>{{ $t('campaigns.views') }}</label>
|
||||
<label for="#">{{ $t('campaigns.views') }}</label>
|
||||
<span>{{ $utils.formatNumber(props.row.views) }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<label>{{ $t('campaigns.clicks') }}</label>
|
||||
<label for="#">{{ $t('campaigns.clicks') }}</label>
|
||||
<span>{{ $utils.formatNumber(props.row.clicks) }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<label>{{ $t('campaigns.sent') }}</label>
|
||||
<label for="#">{{ $t('campaigns.sent') }}</label>
|
||||
<span>
|
||||
{{ $utils.formatNumber(stats.sent) }} /
|
||||
{{ $utils.formatNumber(stats.toSend) }}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<label>{{ $t('globals.terms.bounces') }}</label>
|
||||
<label for="#">{{ $t('globals.terms.bounces') }}</label>
|
||||
<span>
|
||||
<router-link :to="{name: 'bounces', query: { campaign_id: props.row.id }}">
|
||||
<router-link :to="{ name: 'bounces', query: { campaign_id: props.row.id } }">
|
||||
{{ $utils.formatNumber(props.row.bounces) }}
|
||||
</router-link>
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="stats.rate">
|
||||
<label><b-icon icon="speedometer" size="is-small"></b-icon></label>
|
||||
<label for="#"><b-icon icon="speedometer" size="is-small" /></label>
|
||||
<span class="send-rate">
|
||||
<b-tooltip
|
||||
:label="`${stats.netRate} / ${$t('campaigns.rateMinuteShort')} @
|
||||
${$utils.duration(stats.startedAt, stats.updatedAt)}`"
|
||||
type="is-dark">
|
||||
<b-tooltip :label="`${stats.netRate} / ${$t('campaigns.rateMinuteShort')} @
|
||||
${$utils.duration(stats.startedAt, stats.updatedAt)}`" type="is-dark">
|
||||
{{ stats.rate.toFixed(0) }} / {{ $t('campaigns.rateMinuteShort') }}
|
||||
</b-tooltip>
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="isRunning(props.row.id)">
|
||||
<label>{{ $t('campaigns.progress') }}
|
||||
<label for="#">
|
||||
{{ $t('campaigns.progress') }}
|
||||
<span class="spinner is-tiny">
|
||||
<b-loading :is-full-page="false" active />
|
||||
</span>
|
||||
|
@ -172,76 +168,78 @@
|
|||
<b-table-column v-slot="props" cell-class="actions" width="15%" align="right">
|
||||
<div>
|
||||
<!-- start / pause / resume / scheduled -->
|
||||
<a href="" v-if="canStart(props.row)"
|
||||
@click.prevent="$utils.confirm(null,
|
||||
() => changeCampaignStatus(props.row, 'running'))" data-cy="btn-start">
|
||||
<a v-if="canStart(props.row)" href="#"
|
||||
@click.prevent="$utils.confirm(null, () => changeCampaignStatus(props.row, 'running'))" data-cy="btn-start"
|
||||
:aria-label="$t('campaigns.start')">
|
||||
<b-tooltip :label="$t('campaigns.start')" type="is-dark">
|
||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a href="" v-if="canPause(props.row)"
|
||||
@click.prevent="$utils.confirm(null,
|
||||
() => changeCampaignStatus(props.row, 'paused'))" data-cy="btn-pause">
|
||||
<a v-if="canPause(props.row)" href="#"
|
||||
@click.prevent="$utils.confirm(null, () => changeCampaignStatus(props.row, 'paused'))" data-cy="btn-pause"
|
||||
:aria-label="$t('campaigns.pause')">
|
||||
<b-tooltip :label="$t('campaigns.pause')" type="is-dark">
|
||||
<b-icon icon="pause-circle-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a href="" v-if="canResume(props.row)"
|
||||
@click.prevent="$utils.confirm(null,
|
||||
() => changeCampaignStatus(props.row, 'running'))" data-cy="btn-resume">
|
||||
<a v-if="canResume(props.row)" href="#"
|
||||
@click.prevent="$utils.confirm(null, () => changeCampaignStatus(props.row, 'running'))" data-cy="btn-resume"
|
||||
:aria-label="$t('campaigns.send')">
|
||||
<b-tooltip :label="$t('campaigns.send')" type="is-dark">
|
||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a href="" v-if="canSchedule(props.row)"
|
||||
@click.prevent="$utils.confirm($t('campaigns.confirmSchedule'),
|
||||
() => changeCampaignStatus(props.row, 'scheduled'))" data-cy="btn-schedule">
|
||||
<a v-if="canSchedule(props.row)" href="#"
|
||||
@click.prevent="$utils.confirm($t('campaigns.confirmSchedule'), () => changeCampaignStatus(props.row, 'scheduled'))"
|
||||
data-cy="btn-schedule" :aria-label="$t('campaigns.schedule')">
|
||||
<b-tooltip :label="$t('campaigns.schedule')" type="is-dark">
|
||||
<b-icon icon="clock-start" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
|
||||
<!-- placeholder for finished campaigns -->
|
||||
<a v-if="!canCancel(props.row)
|
||||
&& !canSchedule(props.row) && !canStart(props.row)" data-disabled>
|
||||
<a v-if="!canCancel(props.row) && !canSchedule(props.row) && !canStart(props.row)" href="#" data-disabled
|
||||
aria-label=" ">
|
||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||
</a>
|
||||
|
||||
<a href="" v-if="canCancel(props.row)"
|
||||
@click.prevent="$utils.confirm(null,
|
||||
() => changeCampaignStatus(props.row, 'cancelled'))"
|
||||
data-cy="btn-cancel">
|
||||
<a v-if="canCancel(props.row)" href="#"
|
||||
@click.prevent="$utils.confirm(null, () => changeCampaignStatus(props.row, 'cancelled'))" data-cy="btn-cancel"
|
||||
:aria-label="$t('globals.buttons.cancel')">
|
||||
<b-tooltip :label="$t('globals.buttons.cancel')" type="is-dark">
|
||||
<b-icon icon="cancel" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a v-else data-disabled>
|
||||
<a v-else href="#" data-disabled aria-label=" ">
|
||||
<b-icon icon="cancel" size="is-small" />
|
||||
</a>
|
||||
|
||||
<a href="" @click.prevent="previewCampaign(props.row)" data-cy="btn-preview">
|
||||
<a href="#" @click.prevent="previewCampaign(props.row)" data-cy="btn-preview"
|
||||
:aria-label="$t('campaigns.preview')">
|
||||
<b-tooltip :label="$t('campaigns.preview')" type="is-dark">
|
||||
<b-icon icon="file-find-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a href="" @click.prevent="$utils.prompt($t('globals.buttons.clone'),
|
||||
{ placeholder: $t('globals.fields.name'),
|
||||
value: $t('campaigns.copyOf', { name: props.row.name }) },
|
||||
(name) => cloneCampaign(name, props.row))"
|
||||
data-cy="btn-clone">
|
||||
<a href="#" @click.prevent="$utils.prompt($t('globals.buttons.clone'),
|
||||
{
|
||||
placeholder: $t('globals.fields.name'),
|
||||
value: $t('campaigns.copyOf', { name: props.row.name }),
|
||||
},
|
||||
(name) => cloneCampaign(name, props.row))" data-cy="btn-clone"
|
||||
:aria-label="$t('globals.buttons.clone')">
|
||||
<b-tooltip :label="$t('globals.buttons.clone')" type="is-dark">
|
||||
<b-icon icon="file-multiple-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<router-link :to="{ name: 'campaignAnalytics', query: { 'id': props.row.id }}">
|
||||
<router-link :to="{ name: 'campaignAnalytics', query: { id: props.row.id } }">
|
||||
<b-tooltip :label="$t('globals.terms.analytics')" type="is-dark">
|
||||
<b-icon icon="chart-bar" size="is-small" />
|
||||
</b-tooltip>
|
||||
</router-link>
|
||||
<a href=""
|
||||
@click.prevent="$utils.confirm($t('campaigns.confirmDelete', { name: props.row.name }),
|
||||
() => deleteCampaign(props.row))" data-cy="btn-delete">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
<a href="#"
|
||||
@click.prevent="$utils.confirm($t('campaigns.confirmDelete', { name: props.row.name }), () => deleteCampaign(props.row))"
|
||||
data-cy="btn-delete" :aria-label="$t('globals.buttons.delete')">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</a>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
@ -251,11 +249,8 @@
|
|||
</template>
|
||||
</b-table>
|
||||
|
||||
<campaign-preview v-if="previewItem"
|
||||
type='campaign'
|
||||
:id="previewItem.id"
|
||||
:title="previewItem.name"
|
||||
@close="closePreview"></campaign-preview>
|
||||
<campaign-preview v-if="previewItem" type="campaign" :id="previewItem.id" :title="previewItem.name"
|
||||
@close="closePreview" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<section class="dashboard content">
|
||||
<header class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<h1 class="title is-5">{{ $utils.niceDate(new Date()) }}</h1>
|
||||
<h1 class="title is-5">
|
||||
{{ $utils.niceDate(new Date()) }}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
@ -26,19 +28,19 @@
|
|||
<div class="column is-6">
|
||||
<ul class="no has-text-grey">
|
||||
<li>
|
||||
<label>{{ $utils.niceNumber(counts.lists.public) }}</label>
|
||||
<label for="#">{{ $utils.niceNumber(counts.lists.public) }}</label>
|
||||
{{ $t('lists.types.public') }}
|
||||
</li>
|
||||
<li>
|
||||
<label>{{ $utils.niceNumber(counts.lists.private) }}</label>
|
||||
<label for="#">{{ $utils.niceNumber(counts.lists.private) }}</label>
|
||||
{{ $t('lists.types.private') }}
|
||||
</li>
|
||||
<li>
|
||||
<label>{{ $utils.niceNumber(counts.lists.optinSingle) }}</label>
|
||||
<label for="#">{{ $utils.niceNumber(counts.lists.optinSingle) }}</label>
|
||||
{{ $t('lists.optins.single') }}
|
||||
</li>
|
||||
<li>
|
||||
<label>{{ $utils.niceNumber(counts.lists.optinDouble) }}</label>
|
||||
<label for="#">{{ $utils.niceNumber(counts.lists.optinDouble) }}</label>
|
||||
{{ $t('lists.optins.double') }}
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -60,7 +62,7 @@
|
|||
<div class="column is-6">
|
||||
<ul class="no has-text-grey">
|
||||
<li v-for="(num, status) in counts.campaigns.byStatus" :key="status">
|
||||
<label :data-cy="`campaigns-${status}`">{{ num }}</label>
|
||||
<label for="#" :data-cy="`campaigns-${status}`">{{ num }}</label>
|
||||
{{ $t(`campaigns.status.${status}`) }}
|
||||
<span v-if="status === 'running'" class="spinner is-tiny">
|
||||
<b-loading :is-full-page="false" active />
|
||||
|
@ -89,11 +91,11 @@
|
|||
<div class="column is-6">
|
||||
<ul class="no has-text-grey">
|
||||
<li>
|
||||
<label>{{ $utils.niceNumber(counts.subscribers.blocklisted) }}</label>
|
||||
<label for="#">{{ $utils.niceNumber(counts.subscribers.blocklisted) }}</label>
|
||||
{{ $t('subscribers.status.blocklisted') }}
|
||||
</li>
|
||||
<li>
|
||||
<label>{{ $utils.niceNumber(counts.subscribers.orphans) }}</label>
|
||||
<label for="#">{{ $utils.niceNumber(counts.subscribers.orphans) }}</label>
|
||||
{{ $t('dashboard.orphanSubs') }}
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -119,14 +121,16 @@
|
|||
<article class="tile is-child notification charts">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<h3 class="title is-size-6">{{ $t('dashboard.campaignViews') }}</h3><br />
|
||||
<div ref="chart-views"></div>
|
||||
<h3 class="title is-size-6">
|
||||
{{ $t('dashboard.campaignViews') }}
|
||||
</h3><br />
|
||||
<div ref="chart-views" />
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<h3 class="title is-size-6 has-text-right">
|
||||
{{ $t('dashboard.linkClicks') }}
|
||||
</h3><br />
|
||||
<div ref="chart-clicks"></div>
|
||||
<div ref="chart-clicks" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
@ -138,13 +142,13 @@
|
|||
</template>
|
||||
|
||||
<style lang="css">
|
||||
@import "~c3/c3.css";
|
||||
@import "c3/c3.css";
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import c3 from 'c3';
|
||||
import dayjs from 'dayjs';
|
||||
import Vue from 'vue';
|
||||
import { colors } from '../constants';
|
||||
|
||||
export default Vue.extend({
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<section class="forms content relative">
|
||||
<h1 class="title is-4">{{ $t('forms.title') }}</h1>
|
||||
<h1 class="title is-4">
|
||||
{{ $t('forms.title') }}
|
||||
</h1>
|
||||
<hr />
|
||||
|
||||
<b-loading v-if="loading.lists" :active="loading.lists" :is-full-page="false" />
|
||||
|
@ -16,8 +18,9 @@
|
|||
<b-loading :active="loading.lists" :is-full-page="false" />
|
||||
<ul class="no" data-cy="lists">
|
||||
<li v-for="l in publicLists" :key="l.id">
|
||||
<b-checkbox v-model="checked"
|
||||
:native-value="l.uuid">{{ l.name }}</b-checkbox>
|
||||
<b-checkbox v-model="checked" :native-value="l.uuid">
|
||||
{{ l.name }}
|
||||
</b-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@ -25,8 +28,10 @@
|
|||
<hr />
|
||||
<h4>{{ $t('forms.publicSubPage') }}</h4>
|
||||
<p>
|
||||
<a :href="`${settings['app.root_url']}/subscription/form`"
|
||||
target="_blank" data-cy="url">{{ settings['app.root_url'] }}/subscription/form</a>
|
||||
<a :href="`${settings['app.root_url']}/subscription/form`" target="_blank" rel="noopener noreferer"
|
||||
data-cy="url">
|
||||
{{ settings['app.root_url'] }}/subscription/form
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -38,11 +43,11 @@
|
|||
|
||||
<!-- eslint-disable max-len -->
|
||||
<pre v-if="checked.length > 0"><form method="post" action="{{ settings['app.root_url'] }}/subscription/form" class="listmonk-form">
|
||||
<div>
|
||||
<h3>Subscribe</h3>
|
||||
<input type="hidden" name="nonce" />
|
||||
<p><input type="email" name="email" required placeholder="{{ $t('subscribers.email') }}" /></p>
|
||||
<p><input type="text" name="name" placeholder="{{ $t('public.subName') }}" /></p>
|
||||
<div>
|
||||
<h3>Subscribe</h3>
|
||||
<input type="hidden" name="nonce" />
|
||||
<p><input type="email" name="email" required placeholder="{{ $t('subscribers.email') }}" /></p>
|
||||
<p><input type="text" name="name" placeholder="{{ $t('public.subName') }}" /></p>
|
||||
<template v-for="l in publicLists"><span v-if="l.uuid in selected" :key="l.id" :set="id = l.uuid.substr(0, 5)">
|
||||
<p>
|
||||
<input id="{{ id }}" type="checkbox" name="l" checked value="{{ l.uuid }}" />
|
||||
|
@ -59,7 +64,6 @@
|
|||
</form></pre>
|
||||
</div>
|
||||
</div><!-- columns -->
|
||||
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<template>
|
||||
<section class="import">
|
||||
<h1 class="title is-4">{{ $t('import.title') }}</h1>
|
||||
<b-loading :active="isLoading"></b-loading>
|
||||
<h1 class="title is-4">
|
||||
{{ $t('import.title') }}
|
||||
</h1>
|
||||
<b-loading :active="isLoading" />
|
||||
|
||||
<section v-if="isFree()" class="wrap">
|
||||
<form @submit.prevent="onSubmit" class="box">
|
||||
|
@ -10,39 +12,29 @@
|
|||
<div class="column">
|
||||
<b-field :label="$t('import.mode')" :addons="false">
|
||||
<div>
|
||||
<b-radio v-model="form.mode" name="mode"
|
||||
native-value="subscribe"
|
||||
data-cy="check-subscribe">{{ $t('import.subscribe') }}</b-radio>
|
||||
<b-radio v-model="form.mode" name="mode" native-value="subscribe" data-cy="check-subscribe">
|
||||
{{ $t('import.subscribe') }}
|
||||
</b-radio>
|
||||
<br />
|
||||
<b-radio v-model="form.mode" name="mode"
|
||||
native-value="blocklist"
|
||||
data-cy="check-blocklist">{{ $t('import.blocklist') }}</b-radio>
|
||||
<b-radio v-model="form.mode" name="mode" native-value="blocklist" data-cy="check-blocklist">
|
||||
{{ $t('import.blocklist') }}
|
||||
</b-radio>
|
||||
</div>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('globals.fields.status')" :addons="false">
|
||||
<template v-if="form.mode === 'subscribe'">
|
||||
<b-radio
|
||||
v-model="form.subStatus"
|
||||
name="subStatus"
|
||||
native-value="unconfirmed"
|
||||
<b-radio v-model="form.subStatus" name="subStatus" native-value="unconfirmed"
|
||||
data-cy="check-unconfirmed">
|
||||
{{ $t('subscribers.status.unconfirmed') }}
|
||||
</b-radio>
|
||||
<b-radio
|
||||
v-model="form.subStatus"
|
||||
name="subStatus"
|
||||
native-value="confirmed"
|
||||
data-cy="check-confirmed">
|
||||
<b-radio v-model="form.subStatus" name="subStatus" native-value="confirmed" data-cy="check-confirmed">
|
||||
{{ $t('subscribers.status.confirmed') }}
|
||||
</b-radio>
|
||||
</template>
|
||||
|
||||
<b-radio v-else
|
||||
v-model="form.subStatus"
|
||||
name="subStatus"
|
||||
native-value="unsubscribed"
|
||||
<b-radio v-else v-model="form.subStatus" name="subStatus" native-value="unsubscribed"
|
||||
data-cy="check-unsubscribed">
|
||||
{{ $t('subscribers.status.unsubscribed') }}
|
||||
</b-radio>
|
||||
|
@ -50,8 +42,7 @@
|
|||
</div>
|
||||
|
||||
<div class="column">
|
||||
<b-field v-if="form.mode === 'subscribe'"
|
||||
:label="$t('import.overwrite')"
|
||||
<b-field v-if="form.mode === 'subscribe'" :label="$t('import.overwrite')"
|
||||
:message="$t('import.overwriteHelp')">
|
||||
<div>
|
||||
<b-switch v-model="form.overwrite" name="overwrite" data-cy="overwrite" />
|
||||
|
@ -60,28 +51,22 @@
|
|||
</div>
|
||||
|
||||
<div class="column">
|
||||
<b-field :label="$t('import.csvDelim')" :message="$t('import.csvDelimHelp')"
|
||||
class="delimiter">
|
||||
<b-field :label="$t('import.csvDelim')" :message="$t('import.csvDelimHelp')" 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="$t('globals.terms.lists')"
|
||||
:placeholder="$t('import.listSubHelp')"
|
||||
:message="$t('import.listSubHelp')"
|
||||
v-model="form.lists"
|
||||
:selected="form.lists"
|
||||
:all="lists.results"
|
||||
></list-selector>
|
||||
<list-selector v-if="form.mode === 'subscribe'" :label="$t('globals.terms.lists')"
|
||||
:placeholder="$t('import.listSubHelp')" :message="$t('import.listSubHelp')" v-model="form.lists"
|
||||
:selected="form.lists" :all="lists.results" />
|
||||
<hr />
|
||||
|
||||
<b-field :label="$t('import.csvFile')" label-position="on-border">
|
||||
<b-upload v-model="form.file" drag-drop expanded>
|
||||
<div class="has-text-centered section">
|
||||
<p>
|
||||
<b-icon icon="file-upload-outline" size="is-large"></b-icon>
|
||||
<b-icon icon="file-upload-outline" size="is-large" />
|
||||
</p>
|
||||
<p>{{ $t('import.csvFileHelp') }}</p>
|
||||
</div>
|
||||
|
@ -94,62 +79,66 @@
|
|||
</div>
|
||||
<div class="buttons">
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
:disabled="!form.file || (form.mode === 'subscribe' && form.lists.length === 0)"
|
||||
:loading="isProcessing">{{ $t('import.upload') }}</b-button>
|
||||
:disabled="!form.file || (form.mode === 'subscribe' && form.lists.length === 0)" :loading="isProcessing">
|
||||
{{ $t('import.upload') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<br /><br />
|
||||
|
||||
<div class="import-help">
|
||||
<h5 class="title is-size-6">{{ $t('import.instructions') }}</h5>
|
||||
<h5 class="title is-size-6">
|
||||
{{ $t('import.instructions') }}
|
||||
</h5>
|
||||
<p>{{ $t('import.instructionsHelp') }}</p>
|
||||
<br />
|
||||
<blockquote className="csv-example">
|
||||
<code className="csv-headers">
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>attributes</span>
|
||||
</code>
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>attributes</span>
|
||||
</code>
|
||||
</blockquote>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5 class="title is-size-6">{{ $t('import.csvExample') }}</h5>
|
||||
<h5 class="title is-size-6">
|
||||
{{ $t('import.csvExample') }}
|
||||
</h5>
|
||||
<blockquote className="csv-example">
|
||||
<code className="csv-headers">
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>attributes</span>
|
||||
</code><br />
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>attributes</span>
|
||||
</code><br />
|
||||
<code className="csv-row">
|
||||
<span>user1@mail.com,</span>
|
||||
<span>"User One",</span>
|
||||
<span>"{""age"": 42, ""planet"": ""Mars""}"</span>
|
||||
</code><br />
|
||||
<span>user1@mail.com,</span>
|
||||
<span>"User One",</span>
|
||||
<span>"{""age"": 42, ""planet"": ""Mars""}"</span>
|
||||
</code><br />
|
||||
<code className="csv-row">
|
||||
<span>user2@mail.com,</span>
|
||||
<span>"User Two",</span>
|
||||
<span>"{""age"": 24, ""job"": ""Time Traveller""}"</span>
|
||||
</code>
|
||||
<span>user2@mail.com,</span>
|
||||
<span>"User Two",</span>
|
||||
<span>"{""age"": 24, ""job"": ""Time Traveller""}"</span>
|
||||
</code>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section><!-- upload //-->
|
||||
|
||||
<section v-if="isRunning() || isDone()" class="wrap status box has-text-centered">
|
||||
<b-progress :value="progress" show-value type="is-success"></b-progress>
|
||||
<b-progress :value="progress" show-value type="is-success" />
|
||||
<br />
|
||||
<p :class="['is-size-5', 'is-capitalized',
|
||||
{'has-text-success': status.status === 'finished'},
|
||||
{'has-text-danger': (status.status === 'failed' || status.status === 'stopped')}]">
|
||||
{{ status.status }}</p>
|
||||
<p
|
||||
:class="['is-size-5', 'is-capitalized', { 'has-text-success': status.status === 'finished' }, { 'has-text-danger': (status.status === 'failed' || status.status === 'stopped') }]">
|
||||
{{ status.status }}
|
||||
</p>
|
||||
|
||||
<p>{{ $t('import.recordsCount', { num: status.imported, total: status.total }) }}</p>
|
||||
<br />
|
||||
|
||||
<p>
|
||||
<b-button @click="stopImport" :loading="isProcessing" icon-left="file-upload-outline"
|
||||
type="is-primary">
|
||||
<b-button @click="stopImport" :loading="isProcessing" icon-left="file-upload-outline" type="is-primary">
|
||||
{{ isDone() ? $t('import.importDone') : $t('import.stopImport') }}
|
||||
</b-button>
|
||||
</p>
|
||||
|
@ -175,8 +164,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
props: {
|
||||
data: {},
|
||||
isEditing: null,
|
||||
data: { type: Object, default: () => { } },
|
||||
isEditing: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -9,45 +9,58 @@
|
|||
<b-tag v-if="isEditing" :class="[data.type, 'is-pulled-right']">
|
||||
{{ $t(`lists.types.${data.type}`) }}
|
||||
</b-tag>
|
||||
<h4 v-if="isEditing">{{ data.name }}</h4>
|
||||
<h4 v-else>{{ $t('lists.newList') }}</h4>
|
||||
<h4 v-if="isEditing">
|
||||
{{ data.name }}
|
||||
</h4>
|
||||
<h4 v-else>
|
||||
{{ $t('lists.newList') }}
|
||||
</h4>
|
||||
</header>
|
||||
<section expanded class="modal-card-body">
|
||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" name="name"
|
||||
:placeholder="$t('globals.fields.name')" required></b-input>
|
||||
:placeholder="$t('globals.fields.name')" required />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('lists.type')" label-position="on-border"
|
||||
:message="$t('lists.typeHelp')">
|
||||
<b-field :label="$t('lists.type')" label-position="on-border" :message="$t('lists.typeHelp')">
|
||||
<b-select v-model="form.type" name="type" :placeholder="$t('lists.typeHelp')" required>
|
||||
<option value="private">{{ $t('lists.types.private') }}</option>
|
||||
<option value="public">{{ $t('lists.types.public') }}</option>
|
||||
<option value="private">
|
||||
{{ $t('lists.types.private') }}
|
||||
</option>
|
||||
<option value="public">
|
||||
{{ $t('lists.types.public') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('lists.optin')" label-position="on-border"
|
||||
:message="$t('lists.optinHelp')">
|
||||
<b-field :label="$t('lists.optin')" label-position="on-border" :message="$t('lists.optinHelp')">
|
||||
<b-select v-model="form.optin" name="optin" placeholder="Opt-in type" required>
|
||||
<option value="single">{{ $t('lists.optins.single') }}</option>
|
||||
<option value="double">{{ $t('lists.optins.double') }}</option>
|
||||
<option value="single">
|
||||
{{ $t('lists.optins.single') }}
|
||||
</option>
|
||||
<option value="double">
|
||||
{{ $t('lists.optins.double') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('globals.terms.tags')" label-position="on-border">
|
||||
<b-taginput v-model="form.tags" name="tags" ellipsis
|
||||
icon="tag-outline" :placeholder="$t('globals.terms.tags')"></b-taginput>
|
||||
<b-taginput v-model="form.tags" name="tags" ellipsis icon="tag-outline"
|
||||
:placeholder="$t('globals.terms.tags')" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('globals.fields.description')" label-position="on-border">
|
||||
<b-input :maxlength="2000" v-model="form.description" name="description" type="textarea"
|
||||
:placeholder="$t('globals.fields.description')"></b-input>
|
||||
:placeholder="$t('globals.fields.description')" />
|
||||
</b-field>
|
||||
</section>
|
||||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="$parent.close()">{{ $t('globals.buttons.close') }}</b-button>
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
:loading="loading.lists" data-cy="btn-save">{{ $t('globals.buttons.save') }}</b-button>
|
||||
<b-button @click="$parent.close()">
|
||||
{{ $t('globals.buttons.close') }}
|
||||
</b-button>
|
||||
<b-button native-type="submit" type="is-primary" :loading="loading.lists" data-cy="btn-save">
|
||||
{{ $t('globals.buttons.save') }}
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -61,8 +74,8 @@ export default Vue.extend({
|
|||
name: 'ListForm',
|
||||
|
||||
props: {
|
||||
data: {},
|
||||
isEditing: null,
|
||||
data: { type: Object, default: () => ({}) },
|
||||
isEditing: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -9,33 +9,25 @@
|
|||
</div>
|
||||
<div class="column has-text-right">
|
||||
<b-field expanded>
|
||||
<b-button expanded type="is-primary" icon-left="plus" class="btn-new"
|
||||
@click="showNewForm" data-cy="btn-new">
|
||||
<b-button expanded type="is-primary" icon-left="plus" class="btn-new" @click="showNewForm" data-cy="btn-new">
|
||||
{{ $t('globals.buttons.new') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<b-table
|
||||
:data="lists.results"
|
||||
:loading="loading.lists"
|
||||
hoverable default-sort="createdAt"
|
||||
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
||||
:current-page="queryParams.page" :per-page="lists.perPage" :total="lists.total"
|
||||
backend-sorting @sort="onSort"
|
||||
>
|
||||
<b-table :data="lists.results" :loading="loading.lists" hoverable default-sort="createdAt" paginated
|
||||
backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
|
||||
:per-page="lists.perPage" :total="lists.total" backend-sorting @sort="onSort">
|
||||
<template #top-left>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<form @submit.prevent="getLists">
|
||||
<div>
|
||||
<b-field>
|
||||
<b-input v-model="queryParams.query" name="query" expanded
|
||||
icon="magnify" ref="query" data-cy="query" />
|
||||
<b-input v-model="queryParams.query" name="query" expanded icon="magnify" ref="query" data-cy="query" />
|
||||
<p class="controls">
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify"
|
||||
data-cy="btn-query" />
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify" data-cy="btn-query" />
|
||||
</p>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -44,24 +36,23 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')"
|
||||
header-class="cy-name" sortable width="25%"
|
||||
paginated backend-pagination pagination-position="both"
|
||||
:td-attrs="$utils.tdID"
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" header-class="cy-name" sortable
|
||||
width="25%" paginated backend-pagination pagination-position="both" :td-attrs="$utils.tdID"
|
||||
@page-change="onPageChange">
|
||||
<div>
|
||||
<a :href="`/lists/${props.row.id}`"
|
||||
@click.prevent="showEditForm(props.row)">
|
||||
<a :href="`/lists/${props.row.id}`" @click.prevent="showEditForm(props.row)">
|
||||
{{ props.row.name }}
|
||||
</a>
|
||||
<b-taglist>
|
||||
<b-tag class="is-small" v-for="t in props.row.tags" :key="t">{{ t }}</b-tag>
|
||||
<b-tag class="is-small" v-for="t in props.row.tags" :key="t">
|
||||
{{ t }}
|
||||
</b-tag>
|
||||
</b-taglist>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="type" :label="$t('globals.fields.type')"
|
||||
header-class="cy-type" sortable width="15%">
|
||||
<b-table-column v-slot="props" field="type" :label="$t('globals.fields.type')" header-class="cy-type" sortable
|
||||
width="15%">
|
||||
<div class="tags">
|
||||
<b-tag :class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||
{{ $t(`lists.types.${props.row.type}`) }}
|
||||
|
@ -69,15 +60,14 @@
|
|||
{{ ' ' }}
|
||||
|
||||
<b-tag :class="props.row.optin" :data-cy="`optin-${props.row.optin}`">
|
||||
<b-icon :icon="props.row.optin === 'double' ?
|
||||
'account-check-outline' : 'account-off-outline'" size="is-small" />
|
||||
<b-icon :icon="props.row.optin === 'double' ? 'account-check-outline' : 'account-off-outline'"
|
||||
size="is-small" />
|
||||
{{ ' ' }}
|
||||
{{ $t(`lists.optins.${props.row.optin}`) }}
|
||||
</b-tag>{{ ' ' }}
|
||||
|
||||
<a v-if="props.row.optin === 'double'" class="is-size-7 send-optin"
|
||||
href="#" @click="$utils.confirm(null, () => createOptinCampaign(props.row))"
|
||||
data-cy="btn-send-optin-campaign">
|
||||
<a v-if="props.row.optin === 'double'" class="is-size-7 send-optin" href="#"
|
||||
@click="$utils.confirm(null, () => createOptinCampaign(props.row))" data-cy="btn-send-optin-campaign">
|
||||
<b-tooltip :label="$t('lists.sendOptinCampaign')" type="is-dark">
|
||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||
{{ $t('lists.sendOptinCampaign') }}
|
||||
|
@ -86,21 +76,18 @@
|
|||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="subscriber_count"
|
||||
:label="$t('globals.terms.subscribers')" header-class="cy-subscribers"
|
||||
numeric sortable centered>
|
||||
<b-table-column v-slot="props" field="subscriber_count" :label="$t('globals.terms.subscribers')"
|
||||
header-class="cy-subscribers" numeric sortable centered>
|
||||
<router-link :to="`/subscribers/lists/${props.row.id}`">
|
||||
{{ $utils.formatNumber(props.row.subscriberCount) }}
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="subscriber_counts"
|
||||
header-class="cy-subscribers" width="10%">
|
||||
<b-table-column v-slot="props" field="subscriber_counts" header-class="cy-subscribers" width="10%">
|
||||
<div class="fields stats">
|
||||
<p v-for="(count, status) in filterStatuses(props.row)" :key="status">
|
||||
<label>{{ $tc(`subscribers.status.${status}`, count) }}</label>
|
||||
<router-link :to="`/subscribers/lists/${props.row.id}?subscription_status=${status}`"
|
||||
:class="status">
|
||||
<label for="#">{{ $tc(`subscribers.status.${status}`, count) }}</label>
|
||||
<router-link :to="`/subscribers/lists/${props.row.id}?subscription_status=${status}`" :class="status">
|
||||
{{ $utils.formatNumber(count) }}
|
||||
</router-link>
|
||||
</p>
|
||||
|
@ -109,11 +96,11 @@
|
|||
|
||||
<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')"
|
||||
header-class="cy-created_at" sortable>
|
||||
{{ $utils.niceDate(props.row.createdAt) }}
|
||||
{{ $utils.niceDate(props.row.createdAt) }}
|
||||
</b-table-column>
|
||||
<b-table-column v-slot="props" field="updated_at" :label="$t('globals.fields.updatedAt')"
|
||||
header-class="cy-updated_at" sortable>
|
||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="actions" align="right">
|
||||
|
@ -124,20 +111,21 @@
|
|||
</b-tooltip>
|
||||
</router-link>
|
||||
|
||||
<a href="" @click.prevent="showEditForm(props.row)" data-cy="btn-edit">
|
||||
<a href="#" @click.prevent="showEditForm(props.row)" data-cy="btn-edit"
|
||||
:aria-label="$t('globals.buttons.edit')">
|
||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||
<b-icon icon="pencil-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
|
||||
<router-link :to="{name: 'import', query: { list_id: props.row.id }}"
|
||||
data-cy="btn-import">
|
||||
<router-link :to="{ name: 'import', query: { list_id: props.row.id } }" data-cy="btn-import">
|
||||
<b-tooltip :label="$t('import.title')" type="is-dark">
|
||||
<b-icon icon="file-upload-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</router-link>
|
||||
|
||||
<a href="" @click.prevent="deleteList(props.row)" data-cy="btn-delete">
|
||||
<a href="#" @click.prevent="deleteList(props.row)" data-cy="btn-delete"
|
||||
:aria-label="$t('globals.buttons.delete')">
|
||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
|
@ -146,14 +134,13 @@
|
|||
</b-table-column>
|
||||
|
||||
<template #empty v-if="!loading.lists">
|
||||
<empty-placeholder />
|
||||
<empty-placeholder />
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<!-- Add / edit form modal -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="600"
|
||||
@close="onFormClose">
|
||||
<list-form :data="curItem" :isEditing="isEditing" @finished="formFinished"></list-form>
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="600" @close="onFormClose">
|
||||
<list-form :data="curItem" :is-editing="isEditing" @finished="formFinished" />
|
||||
</b-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -161,8 +148,8 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import ListForm from './ListForm.vue';
|
||||
import EmptyPlaceholder from '../components/EmptyPlaceholder.vue';
|
||||
import ListForm from './ListForm.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<section class="logs content relative">
|
||||
<h1 class="title is-4">{{ $t('logs.title') }}</h1>
|
||||
<h1 class="title is-4">
|
||||
{{ $t('logs.title') }}
|
||||
</h1>
|
||||
<hr />
|
||||
<log-view :loading="loading.logs" :lines="lines"></log-view>
|
||||
<log-view :loading="loading.logs" :lines="lines" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<section class="maintenance wrap">
|
||||
<h1 class="title is-4">{{ $t('maintenance.title') }}</h1>
|
||||
<h1 class="title is-4">
|
||||
{{ $t('maintenance.title') }}
|
||||
</h1>
|
||||
<hr />
|
||||
<p class="has-text-grey">
|
||||
{{ $t('maintenance.help') }}
|
||||
|
@ -8,98 +10,110 @@
|
|||
<br />
|
||||
|
||||
<div class="box">
|
||||
<h4 class="is-size-4">{{ $t('globals.terms.subscribers') }}</h4><br />
|
||||
<h4 class="is-size-4">
|
||||
{{ $t('globals.terms.subscribers') }}
|
||||
</h4><br />
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field label="Data" message="$t('maintenance.orphanHelp')">
|
||||
<b-select v-model="subscriberType" expanded>
|
||||
<option value="orphan">{{ $t('dashboard.orphanSubs') }}</option>
|
||||
<option value="blocklisted">{{ $t('subscribers.status.blocklisted') }}</option>
|
||||
<option value="orphan">
|
||||
{{ $t('dashboard.orphanSubs') }}
|
||||
</option>
|
||||
<option value="blocklisted">
|
||||
{{ $t('subscribers.status.blocklisted') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-5"></div>
|
||||
<div class="column is-5" />
|
||||
<div class="column">
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button class="is-primary" :loading="loading.maintenance"
|
||||
@click="deleteSubscribers" expanded>{{ $t('globals.buttons.delete') }}</b-button>
|
||||
<b-button class="is-primary" :loading="loading.maintenance" @click="deleteSubscribers" expanded>
|
||||
{{ $t('globals.buttons.delete') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- subscribers -->
|
||||
|
||||
<div class="box mt-6">
|
||||
<h4 class="is-size-4">{{ $tc('globals.terms.subscriptions', 2) }}</h4><br />
|
||||
<h4 class="is-size-4">
|
||||
{{ $tc('globals.terms.subscriptions', 2) }}
|
||||
</h4><br />
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field label="Data">
|
||||
<b-select v-model="subscriptionType" expanded>
|
||||
<option value="optin">{{ $t('maintenance.maintenance.unconfirmedOptins') }}</option>
|
||||
<option value="optin">
|
||||
{{ $t('maintenance.maintenance.unconfirmedOptins') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('maintenance.olderThan')">
|
||||
<b-datepicker
|
||||
v-model="subscriptionDate"
|
||||
required expanded
|
||||
icon="calendar-clock"
|
||||
:date-formatter="formatDateTime">
|
||||
</b-datepicker>
|
||||
<b-datepicker v-model="subscriptionDate" required expanded icon="calendar-clock"
|
||||
:date-formatter="formatDateTime" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-1"></div>
|
||||
<div class="column is-1" />
|
||||
<div class="column">
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button class="is-primary" :loading="loading.maintenance"
|
||||
@click="deleteSubscriptions" expanded>{{ $t('globals.buttons.delete') }}</b-button>
|
||||
<b-button class="is-primary" :loading="loading.maintenance" @click="deleteSubscriptions" expanded>
|
||||
{{ $t('globals.buttons.delete') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- subscriptions -->
|
||||
|
||||
<div class="box mt-6">
|
||||
<h4 class="is-size-4">{{ $t('globals.terms.analytics') }}</h4><br />
|
||||
<h4 class="is-size-4">
|
||||
{{ $t('globals.terms.analytics') }}
|
||||
</h4><br />
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field label="Data">
|
||||
<b-select v-model="analyticsType" expanded>
|
||||
<option selected value="all">{{ $t('globals.terms.all') }}</option>
|
||||
<option value="views">{{ $t('dashboard.campaignViews') }}</option>
|
||||
<option value="clicks">{{ $t('dashboard.linkClicks') }}</option>
|
||||
<option selected value="all">
|
||||
{{ $t('globals.terms.all') }}
|
||||
</option>
|
||||
<option value="views">
|
||||
{{ $t('dashboard.campaignViews') }}
|
||||
</option>
|
||||
<option value="clicks">
|
||||
{{ $t('dashboard.linkClicks') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('maintenance.olderThan')">
|
||||
<b-datepicker
|
||||
v-model="analyticsDate"
|
||||
required expanded
|
||||
icon="calendar-clock"
|
||||
:date-formatter="formatDateTime">
|
||||
</b-datepicker>
|
||||
<b-datepicker v-model="analyticsDate" required expanded icon="calendar-clock"
|
||||
:date-formatter="formatDateTime" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-1"></div>
|
||||
<div class="column is-1" />
|
||||
<div class="column">
|
||||
<br />
|
||||
<b-field>
|
||||
<b-button expanded class="is-primary" :loading="loading.maintenance"
|
||||
@click="deleteAnalytics">{{ $t('globals.buttons.delete') }}</b-button>
|
||||
<b-button expanded class="is-primary" :loading="loading.maintenance" @click="deleteAnalytics">
|
||||
{{ $t('globals.buttons.delete') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- analytics -->
|
||||
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from 'dayjs';
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
@ -125,8 +139,10 @@ export default Vue.extend({
|
|||
null,
|
||||
() => {
|
||||
this.$api.deleteGCSubscribers(this.subscriberType).then((data) => {
|
||||
this.$utils.toast(this.$t('globals.messages.deletedCount',
|
||||
{ name: this.$tc('globals.terms.subscribers', 2), num: data.count }));
|
||||
this.$utils.toast(this.$t(
|
||||
'globals.messages.deletedCount',
|
||||
{ name: this.$tc('globals.terms.subscribers', 2), num: data.count },
|
||||
));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -137,8 +153,10 @@ export default Vue.extend({
|
|||
null,
|
||||
() => {
|
||||
this.$api.deleteGCSubscriptions(this.subscriptionDate).then((data) => {
|
||||
this.$utils.toast(this.$t('globals.messages.deletedCount',
|
||||
{ name: this.$tc('globals.terms.subscriptions', 2), num: data.count }));
|
||||
this.$utils.toast(this.$t(
|
||||
'globals.messages.deletedCount',
|
||||
{ name: this.$tc('globals.terms.subscriptions', 2), num: data.count },
|
||||
));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,63 +1,56 @@
|
|||
<template>
|
||||
<section class="media-files">
|
||||
<h1 class="title is-4">{{ $t('media.title') }}
|
||||
<h1 class="title is-4">
|
||||
{{ $t('media.title') }}
|
||||
<span v-if="media.length > 0">({{ media.length }})</span>
|
||||
|
||||
<span class="has-text-grey-light"> / {{ settings['upload.provider'] }}</span>
|
||||
</h1>
|
||||
|
||||
<b-loading :active="isProcessing || loading.media"></b-loading>
|
||||
<b-loading :active="isProcessing || loading.media" />
|
||||
|
||||
<section class="wrap">
|
||||
<form @submit.prevent="onSubmit" class="box">
|
||||
<div>
|
||||
<b-field :label="$t('media.uploadImage')">
|
||||
<b-upload
|
||||
v-model="form.files"
|
||||
drag-drop
|
||||
multiple
|
||||
xaccept=".png,.jpg,.jpeg,.gif,.svg"
|
||||
expanded>
|
||||
<b-upload v-model="form.files" drag-drop multiple xaccept=".png,.jpg,.jpeg,.gif,.svg" expanded>
|
||||
<div class="has-text-centered section">
|
||||
<p>
|
||||
<b-icon icon="file-upload-outline" size="is-large"></b-icon>
|
||||
<b-icon icon="file-upload-outline" size="is-large" />
|
||||
</p>
|
||||
<p>{{ $t('media.uploadHelp') }}</p>
|
||||
</div>
|
||||
</b-upload>
|
||||
</b-field>
|
||||
<div class="tags" v-if="form.files.length > 0">
|
||||
<b-tag v-for="(f, i) in form.files" :key="i" size="is-medium"
|
||||
closable @close="removeUploadFile(i)">
|
||||
<b-tag v-for="(f, i) in form.files" :key="i" size="is-medium" closable @close="removeUploadFile(i)">
|
||||
{{ f.name }}
|
||||
</b-tag>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<b-button native-type="submit" type="is-primary" icon-left="file-upload-outline"
|
||||
:disabled="form.files.length === 0"
|
||||
:loading="isProcessing">{{ $tc('media.upload') }}</b-button>
|
||||
:disabled="form.files.length === 0" :loading="isProcessing">
|
||||
{{ $tc('media.upload') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="wrap gallery mt-6">
|
||||
<b-table :data="media.results" :hoverable="true" :loading="loading.media"
|
||||
default-sort="createdAt" :paginated="true" backend-pagination pagination-position="both"
|
||||
@page-change="onPageChange"
|
||||
:current-page="media.page" :per-page="media.perPage" :total="media.total">
|
||||
|
||||
<b-table :data="media.results" :hoverable="true" :loading="loading.media" default-sort="createdAt" :paginated="true"
|
||||
backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="media.page"
|
||||
:per-page="media.perPage" :total="media.total">
|
||||
<template #top-left>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<form @submit.prevent="onQueryMedia">
|
||||
<div>
|
||||
<b-field>
|
||||
<b-input v-model="queryParams.query" name="query" expanded
|
||||
icon="magnify" ref="query" data-cy="query" />
|
||||
<b-input v-model="queryParams.query" name="query" expanded icon="magnify" ref="query"
|
||||
data-cy="query" />
|
||||
<p class="controls">
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify"
|
||||
data-cy="btn-query" />
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify" data-cy="btn-query" />
|
||||
</p>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -66,32 +59,30 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<b-table-column v-slot="props" field="name" width="40%"
|
||||
:label="$t('globals.fields.name')">
|
||||
<a @click="(e) => onMediaSelect(props.row, e)" :href="props.row.url"
|
||||
target="_blank" class="link" :title="props.row.filename">
|
||||
<b-table-column v-slot="props" field="name" width="40%" :label="$t('globals.fields.name')">
|
||||
<a @click="(e) => onMediaSelect(props.row, e)" :href="props.row.url" target="_blank" rel="noopener noreferer"
|
||||
class="link" :title="props.row.filename">
|
||||
{{ props.row.filename }}
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="thumb" width="30%">
|
||||
<a @click="(e) => onMediaSelect(props.row, e)" :href="props.row.url"
|
||||
target="_blank" class="thumb box">
|
||||
<img v-if="props.row.thumbUrl" :src="props.row.thumbUrl" :title="props.row.filename" />
|
||||
<a @click="(e) => onMediaSelect(props.row, e)" :href="props.row.url" target="_blank" rel="noopener noreferer"
|
||||
class="thumb box">
|
||||
<img v-if="props.row.thumbUrl" :src="props.row.thumbUrl" :title="props.row.filename" alt="" />
|
||||
<span v-else class="ext">
|
||||
{{ props.row.filename.split(".").pop() }}
|
||||
</span>
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="created_at" width="25%"
|
||||
:label="$t('globals.fields.createdAt')" sortable>
|
||||
<b-table-column v-slot="props" field="created_at" width="25%" :label="$t('globals.fields.createdAt')" sortable>
|
||||
{{ $utils.niceDate(props.row.createdAt, true) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="actions" width="5%" cell-class="has-text-right">
|
||||
<a href="" @click.prevent="$utils.confirm(null, () => onDeleteMedia(props.row.id))"
|
||||
data-cy="btn-delete">
|
||||
<a href="#" @click.prevent="$utils.confirm(null, () => onDeleteMedia(props.row.id))" data-cy="btn-delete"
|
||||
:aria-label="$t('globals.buttons.delete')">
|
||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
|
@ -120,7 +111,7 @@ export default Vue.extend({
|
|||
|
||||
props: {
|
||||
isModal: Boolean,
|
||||
type: String,
|
||||
type: { type: String, default: '' },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -4,15 +4,15 @@
|
|||
<b-loading :is-full-page="true" v-if="loading.settings || isLoading" active />
|
||||
<header class="columns page-header">
|
||||
<div class="column is-half">
|
||||
<h1 class="title is-4">{{ $t('settings.title') }}
|
||||
<h1 class="title is-4">
|
||||
{{ $t('settings.title') }}
|
||||
<span class="has-text-grey-light">({{ serverConfig.version }})</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<b-field expanded>
|
||||
<b-button expanded :disabled="!hasFormChanged"
|
||||
type="is-primary" icon-left="content-save-outline" native-type="submit"
|
||||
class="isSaveEnabled" data-cy="btn-save">
|
||||
<b-button expanded :disabled="!hasFormChanged" type="is-primary" icon-left="content-save-outline"
|
||||
native-type="submit" class="isSaveEnabled" data-cy="btn-save">
|
||||
{{ $t('globals.buttons.save') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
|
@ -21,44 +21,43 @@
|
|||
<hr />
|
||||
|
||||
<section class="wrap" v-if="form">
|
||||
<b-tabs type="is-boxed" :animated="false" v-model="tab">
|
||||
<b-tab-item :label="$t('settings.general.name')" label-position="on-border">
|
||||
<general-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- general -->
|
||||
<b-tabs type="is-boxed" :animated="false" v-model="tab">
|
||||
<b-tab-item :label="$t('settings.general.name')" label-position="on-border">
|
||||
<general-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- general -->
|
||||
|
||||
<b-tab-item :label="$t('settings.performance.name')">
|
||||
<performance-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- performance -->
|
||||
<b-tab-item :label="$t('settings.performance.name')">
|
||||
<performance-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- performance -->
|
||||
|
||||
<b-tab-item :label="$t('settings.privacy.name')">
|
||||
<privacy-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- privacy -->
|
||||
<b-tab-item :label="$t('settings.privacy.name')">
|
||||
<privacy-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- privacy -->
|
||||
|
||||
<b-tab-item :label="$t('settings.security.name')">
|
||||
<security-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- security -->
|
||||
<b-tab-item :label="$t('settings.security.name')">
|
||||
<security-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- security -->
|
||||
|
||||
<b-tab-item :label="$t('settings.media.title')">
|
||||
<media-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- media -->
|
||||
<b-tab-item :label="$t('settings.media.title')">
|
||||
<media-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- media -->
|
||||
|
||||
<b-tab-item :label="$t('settings.smtp.name')">
|
||||
<smtp-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- mail servers -->
|
||||
<b-tab-item :label="$t('settings.smtp.name')">
|
||||
<smtp-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- mail servers -->
|
||||
|
||||
<b-tab-item :label="$t('settings.bounces.name')">
|
||||
<bounce-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- bounces -->
|
||||
<b-tab-item :label="$t('settings.bounces.name')">
|
||||
<bounce-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- bounces -->
|
||||
|
||||
<b-tab-item :label="$t('settings.messengers.name')">
|
||||
<messenger-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- messengers -->
|
||||
|
||||
<b-tab-item :label="$t('settings.appearance.name')">
|
||||
<appearance-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- appearance -->
|
||||
</b-tabs>
|
||||
<b-tab-item :label="$t('settings.messengers.name')">
|
||||
<messenger-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- messengers -->
|
||||
|
||||
<b-tab-item :label="$t('settings.appearance.name')">
|
||||
<appearance-settings :form="form" :key="key" />
|
||||
</b-tab-item><!-- appearance -->
|
||||
</b-tabs>
|
||||
</section>
|
||||
</section>
|
||||
</form>
|
||||
|
@ -67,15 +66,15 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import AppearanceSettings from './settings/appearance.vue';
|
||||
import BounceSettings from './settings/bounces.vue';
|
||||
import GeneralSettings from './settings/general.vue';
|
||||
import MediaSettings from './settings/media.vue';
|
||||
import MessengerSettings from './settings/messengers.vue';
|
||||
import PerformanceSettings from './settings/performance.vue';
|
||||
import PrivacySettings from './settings/privacy.vue';
|
||||
import SecuritySettings from './settings/security.vue';
|
||||
import MediaSettings from './settings/media.vue';
|
||||
import SmtpSettings from './settings/smtp.vue';
|
||||
import BounceSettings from './settings/bounces.vue';
|
||||
import MessengerSettings from './settings/messengers.vue';
|
||||
import AppearanceSettings from './settings/appearance.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
|
|
@ -2,50 +2,43 @@
|
|||
<form @submit.prevent="onSubmit">
|
||||
<div class="modal-card" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<h4 class="title is-size-5">{{ $t('subscribers.manageLists') }}</h4>
|
||||
<h4 class="title is-size-5">
|
||||
{{ $t('subscribers.manageLists') }}
|
||||
</h4>
|
||||
</header>
|
||||
|
||||
<section expanded class="modal-card-body">
|
||||
<b-field label="Action">
|
||||
<div>
|
||||
<b-radio v-model="form.action" name="action" native-value="add"
|
||||
data-cy="check-list-add">
|
||||
<b-radio v-model="form.action" name="action" native-value="add" data-cy="check-list-add">
|
||||
{{ $t('globals.buttons.add') }}
|
||||
</b-radio>
|
||||
<b-radio v-model="form.action" name="action" native-value="remove"
|
||||
data-cy="check-list-remove">
|
||||
<b-radio v-model="form.action" name="action" native-value="remove" data-cy="check-list-remove">
|
||||
{{ $t('globals.buttons.remove') }}
|
||||
</b-radio>
|
||||
<b-radio
|
||||
v-model="form.action"
|
||||
name="action"
|
||||
native-value="unsubscribe"
|
||||
data-cy="check-list-unsubscribe"
|
||||
>{{ $t('subscribers.markUnsubscribed') }}</b-radio>
|
||||
<b-radio v-model="form.action" name="action" native-value="unsubscribe" data-cy="check-list-unsubscribe">
|
||||
{{ $t('subscribers.markUnsubscribed') }}
|
||||
</b-radio>
|
||||
</div>
|
||||
</b-field>
|
||||
|
||||
<list-selector
|
||||
label="Target lists"
|
||||
placeholder="Lists to apply to"
|
||||
v-model="form.lists"
|
||||
:selected="form.lists"
|
||||
:all="lists.results"
|
||||
></list-selector>
|
||||
<list-selector label="Target lists" placeholder="Lists to apply to" v-model="form.lists" :selected="form.lists"
|
||||
:all="lists.results" />
|
||||
|
||||
<b-field :message="$t('subscribers.preconfirmHelp')">
|
||||
<b-checkbox v-model="form.preconfirm" data-cy="preconfirm"
|
||||
:native-value="true" :disabled="!hasOptinList">
|
||||
{{ $t('subscribers.preconfirm') }}
|
||||
</b-checkbox>
|
||||
<b-checkbox v-model="form.preconfirm" data-cy="preconfirm" :native-value="true" :disabled="!hasOptinList">
|
||||
{{ $t('subscribers.preconfirm') }}
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="$parent.close()">{{ $t('globals.buttons.close') }}</b-button>
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
:disabled="form.lists.length === 0">{{ $t('globals.buttons.save') }}</b-button>
|
||||
<b-button @click="$parent.close()">
|
||||
{{ $t('globals.buttons.close') }}
|
||||
</b-button>
|
||||
<b-button native-type="submit" type="is-primary" :disabled="form.lists.length === 0">
|
||||
{{ $t('globals.buttons.save') }}
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -62,7 +55,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
props: {
|
||||
numSubscribers: Number,
|
||||
numSubscribers: { type: Number, default: 0 },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -5,8 +5,12 @@
|
|||
<b-tag v-if="isEditing" :class="[data.status, 'is-pulled-right']">
|
||||
{{ $t(`subscribers.status.${data.status}`) }}
|
||||
</b-tag>
|
||||
<h4 v-if="isEditing">{{ data.name }}</h4>
|
||||
<h4 v-else>{{ $t('subscribers.newSubscriber') }}</h4>
|
||||
<h4 v-if="isEditing">
|
||||
{{ data.name }}
|
||||
</h4>
|
||||
<h4 v-else>
|
||||
{{ $t('subscribers.newSubscriber') }}
|
||||
</h4>
|
||||
|
||||
<p v-if="isEditing" class="has-text-grey is-size-7">
|
||||
{{ $t('globals.fields.id') }}: <span data-cy="id">{{ data.id }}</span> /
|
||||
|
@ -17,48 +21,42 @@
|
|||
<section expanded class="modal-card-body">
|
||||
<b-field :label="$t('subscribers.email')" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.email" name="email" :ref="'focus'"
|
||||
:placeholder="$t('subscribers.email')" required></b-input>
|
||||
:placeholder="$t('subscribers.email')" required />
|
||||
</b-field>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-8">
|
||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||
<b-input :maxlength="200" v-model="form.name" name="name"
|
||||
:placeholder="$t('globals.fields.name')"></b-input>
|
||||
<b-input :maxlength="200" v-model="form.name" name="name" :placeholder="$t('globals.fields.name')" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('globals.fields.status')" label-position="on-border"
|
||||
:message="$t('subscribers.blocklistedHelp')">
|
||||
<b-select v-model="form.status" name="status"
|
||||
:placeholder="$t('globals.fields.status')" required expanded>
|
||||
<option value="enabled">{{ $t('subscribers.status.enabled') }}</option>
|
||||
<option value="blocklisted">{{ $t('subscribers.status.blocklisted') }}</option>
|
||||
<b-select v-model="form.status" name="status" :placeholder="$t('globals.fields.status')" required expanded>
|
||||
<option value="enabled">
|
||||
{{ $t('subscribers.status.enabled') }}
|
||||
</option>
|
||||
<option value="blocklisted">
|
||||
{{ $t('subscribers.status.blocklisted') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<list-selector
|
||||
:label="$t('subscribers.lists')"
|
||||
:placeholder="$t('subscribers.listsPlaceholder')"
|
||||
:message="$t('subscribers.listsHelp')"
|
||||
v-model="form.lists"
|
||||
:selected="form.lists"
|
||||
:all="lists.results"
|
||||
></list-selector>
|
||||
<list-selector :label="$t('subscribers.lists')" :placeholder="$t('subscribers.listsPlaceholder')"
|
||||
:message="$t('subscribers.listsHelp')" v-model="form.lists" :selected="form.lists" :all="lists.results" />
|
||||
<div class="columns mb-5">
|
||||
<div class="column is-7">
|
||||
<b-field :message="$t('subscribers.preconfirmHelp')">
|
||||
<b-checkbox v-model="form.preconfirm"
|
||||
:native-value="true" :disabled="!hasOptinList">
|
||||
{{ $t('subscribers.preconfirm') }}
|
||||
</b-checkbox>
|
||||
<b-checkbox v-model="form.preconfirm" :native-value="true" :disabled="!hasOptinList">
|
||||
{{ $t('subscribers.preconfirm') }}
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-5 has-text-right" v-if="isEditing">
|
||||
<a href="" @click.prevent="sendOptinConfirmation"
|
||||
:class="{'is-disabled': !hasOptinList}">
|
||||
<a href="#" @click.prevent="sendOptinConfirmation" :class="{ 'is-disabled': !hasOptinList }">
|
||||
<b-icon icon="email-outline" size="is-small" />
|
||||
{{ $t('subscribers.sendOptinConfirm') }}</a>
|
||||
</div>
|
||||
|
@ -68,8 +66,7 @@
|
|||
<div>
|
||||
<h5>{{ $t('subscribers.attribs') }}</h5>
|
||||
<b-input v-model="form.strAttribs" name="attribs" type="textarea" />
|
||||
<a href="https://listmonk.app/docs/concepts"
|
||||
target="_blank" rel="noopener noreferrer" class="is-size-7">
|
||||
<a href="https://listmonk.app/docs/concepts" target="_blank" rel="noopener noreferrer" class="is-size-7">
|
||||
{{ $t('globals.buttons.learnMore') }} <b-icon icon="link-variant" size="is-small" />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -77,85 +74,81 @@
|
|||
|
||||
<div class="mb-5" v-if="data.lists">
|
||||
<h5>{{ $tc('globals.terms.subscriptions', 2) }} ({{ data.lists.length }})</h5>
|
||||
<b-table :data="data.lists" hoverable default-sort="createdAt" class="subscriptions"
|
||||
>
|
||||
<b-table-column v-slot="props" field="name"
|
||||
:label="$tc('globals.terms.list', 1)">
|
||||
<b-table :data="data.lists" hoverable default-sort="createdAt" class="subscriptions">
|
||||
<b-table-column v-slot="props" field="name" :label="$tc('globals.terms.list', 1)">
|
||||
<div>
|
||||
<router-link :to="`/lists/${props.row.id}`">
|
||||
{{ props.row.name }}
|
||||
</router-link>
|
||||
<br />
|
||||
<b-tag :class="props.row.optin" :data-cy="`optin-${props.row.optin}`">
|
||||
<b-icon :icon="props.row.optin === 'double' ?
|
||||
'account-check-outline' : 'account-off-outline'" size="is-small" />
|
||||
<b-icon :icon="props.row.optin === 'double' ? 'account-check-outline' : 'account-off-outline'"
|
||||
size="is-small" />
|
||||
{{ ' ' }}
|
||||
{{ $t(`lists.optins.${props.row.optin}`) }}
|
||||
</b-tag>{{ ' ' }}
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="status" cell-class="status"
|
||||
:label="$t('globals.fields.status')">
|
||||
<b-table-column v-slot="props" field="status" cell-class="status" :label="$t('globals.fields.status')">
|
||||
<b-tag :class="`status-${props.row.subscriptionStatus}`">
|
||||
{{ $t(`subscribers.status.${props.row.subscriptionStatus}`) }}
|
||||
</b-tag>
|
||||
<template v-if="props.row.optin === 'double'
|
||||
&& props.row.subscriptionMeta.optinIp">
|
||||
<template v-if="props.row.optin === 'double' && props.row.subscriptionMeta.optinIp">
|
||||
<br /><span class="is-size-7">{{ props.row.subscriptionMeta.optinIp }}</span>
|
||||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="createdAt"
|
||||
:label="$t('globals.fields.createdAt')">
|
||||
<b-table-column v-slot="props" field="createdAt" :label="$t('globals.fields.createdAt')">
|
||||
{{ $utils.niceDate(props.row.subscriptionCreatedAt, true) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="updatedAt"
|
||||
:label="$t('globals.fields.updatedAt')">
|
||||
<b-table-column v-slot="props" field="updatedAt" :label="$t('globals.fields.updatedAt')">
|
||||
{{ $utils.niceDate(props.row.subscriptionCreatedAt, true) }}
|
||||
</b-table-column>
|
||||
</b-table>
|
||||
</div>
|
||||
|
||||
<div class="bounces" v-show="bounces.length > 0">
|
||||
<a href="#" class="is-size-6" disabed="true"
|
||||
@click.prevent="toggleBounces">
|
||||
<b-icon icon="email-bounce"></b-icon>
|
||||
<a href="#" class="is-size-6" disabed="true" @click.prevent="toggleBounces">
|
||||
<b-icon icon="email-bounce" />
|
||||
{{ $t('bounces.view') }} ({{ bounces.length }})
|
||||
</a>
|
||||
<a href="#" class="is-size-6 is-pulled-right" disabed="true"
|
||||
@click.prevent="deleteBounces" v-if="isBounceVisible">
|
||||
<b-icon icon="trash-can-outline"></b-icon>
|
||||
<a href="#" class="is-size-6 is-pulled-right" disabed="true" @click.prevent="deleteBounces"
|
||||
v-if="isBounceVisible">
|
||||
<b-icon icon="trash-can-outline" />
|
||||
{{ $t('globals.buttons.delete') }}
|
||||
</a>
|
||||
|
||||
<div v-if="isBounceVisible" class="mt-4">
|
||||
<ol class="is-size-7">
|
||||
<li v-for="b in bounces" :key="b.id" class="mb-2">
|
||||
<div v-if="b.campaign">
|
||||
<router-link :to="{ name: 'bounces', query: { campaign_id: b.campaign.id } }">
|
||||
{{ b.campaign.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
{{ $utils.niceDate(b.createdAt, true) }}
|
||||
<span class="is-pulled-right">
|
||||
<a href="#" @click.prevent="toggleMeta(b.id)">
|
||||
{{ b.source }}
|
||||
<b-icon :icon="visibleMeta[b.id] ? 'arrow-up' : 'arrow-down'" />
|
||||
</a>
|
||||
</span>
|
||||
<span class="is-clearfix"></span>
|
||||
<pre v-if="visibleMeta[b.id]">{{ b.meta }}</pre>
|
||||
<div v-if="b.campaign">
|
||||
<router-link :to="{ name: 'bounces', query: { campaign_id: b.campaign.id } }">
|
||||
{{ b.campaign.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
{{ $utils.niceDate(b.createdAt, true) }}
|
||||
<span class="is-pulled-right">
|
||||
<a href="#" @click.prevent="toggleMeta(b.id)">
|
||||
{{ b.source }}
|
||||
<b-icon :icon="visibleMeta[b.id] ? 'arrow-up' : 'arrow-down'" />
|
||||
</a>
|
||||
</span>
|
||||
<span class="is-clearfix" />
|
||||
<pre v-if="visibleMeta[b.id]">{{ b.meta }}</pre>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="$parent.close()">{{ $t('globals.buttons.close') }}</b-button>
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
:loading="loading.subscribers">{{ $t('globals.buttons.save') }}</b-button>
|
||||
<b-button @click="$parent.close()">
|
||||
{{ $t('globals.buttons.close') }}
|
||||
</b-button>
|
||||
<b-button native-type="submit" type="is-primary" :loading="loading.subscribers">
|
||||
{{ $t('globals.buttons.save') }}
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -174,7 +167,7 @@ export default Vue.extend({
|
|||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
default: () => { },
|
||||
},
|
||||
isEditing: Boolean,
|
||||
},
|
||||
|
@ -316,8 +309,12 @@ export default Vue.extend({
|
|||
try {
|
||||
attribs = JSON.parse(str);
|
||||
} catch (e) {
|
||||
this.$utils.toast(`${this.$t('subscribers.invalidJSON')}: ${e.toString()}`,
|
||||
'is-danger', 3000);
|
||||
this.$utils.toast(
|
||||
`${this.$t('subscribers.invalidJSON')}: ${e.toString()}`,
|
||||
'is-danger',
|
||||
|
||||
3000,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (attribs instanceof Array) {
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<section class="subscribers">
|
||||
<header class="columns page-header">
|
||||
<div class="column is-10">
|
||||
<h1 class="title is-4">{{ $t('globals.terms.subscribers') }}
|
||||
<h1 class="title is-4">
|
||||
{{ $t('globals.terms.subscribers') }}
|
||||
<span v-if="!isNaN(subscribers.total)">
|
||||
(<span data-cy="count">{{ subscribers.total }}</span>)
|
||||
</span>
|
||||
|
@ -13,8 +14,7 @@
|
|||
</div>
|
||||
<div class="column has-text-right">
|
||||
<b-field expanded>
|
||||
<b-button expanded type="is-primary" icon-left="plus"
|
||||
@click="showNewForm" data-cy="btn-new" class="btn-new">
|
||||
<b-button expanded type="is-primary" icon-left="plus" @click="showNewForm" data-cy="btn-new" class="btn-new">
|
||||
{{ $t('globals.buttons.new') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
|
@ -29,32 +29,29 @@
|
|||
<b-field addons>
|
||||
<b-input @input="onSimpleQueryInput" v-model="queryInput" expanded
|
||||
:placeholder="$t('subscribers.queryPlaceholder')" icon="magnify" ref="query"
|
||||
:disabled="isSearchAdvanced" data-cy="search"></b-input>
|
||||
:disabled="isSearchAdvanced" data-cy="search" />
|
||||
<p class="controls">
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify"
|
||||
:disabled="isSearchAdvanced" data-cy="btn-search"></b-button>
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify" :disabled="isSearchAdvanced"
|
||||
data-cy="btn-search" />
|
||||
</p>
|
||||
</b-field>
|
||||
|
||||
<div v-if="isSearchAdvanced">
|
||||
<b-input v-model="queryParams.queryExp"
|
||||
@keydown.native.enter="onAdvancedQueryEnter"
|
||||
type="textarea" ref="queryExp"
|
||||
placeholder="subscribers.name LIKE '%user%' or subscribers.status='blocklisted'"
|
||||
data-cy="query">
|
||||
</b-input>
|
||||
<b-input v-model="queryParams.queryExp" @keydown.native.enter="onAdvancedQueryEnter" type="textarea"
|
||||
ref="queryExp" placeholder="subscribers.name LIKE '%user%' or subscribers.status='blocklisted'"
|
||||
data-cy="query" />
|
||||
<span class="is-size-6 has-text-grey">
|
||||
{{ $t('subscribers.advancedQueryHelp') }}.{{ ' ' }}
|
||||
<a href="https://listmonk.app/docs/querying-and-segmentation"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
<a href="https://listmonk.app/docs/querying-and-segmentation" target="_blank" rel="noopener noreferrer">
|
||||
{{ $t('globals.buttons.learnMore') }}.
|
||||
</a>
|
||||
</span>
|
||||
<div class="buttons">
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
icon-left="magnify" data-cy="btn-query">{{ $t('subscribers.query') }}</b-button>
|
||||
<b-button @click.prevent="toggleAdvancedSearch" icon-left="cancel"
|
||||
data-cy="btn-query-reset">
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify" data-cy="btn-query">
|
||||
{{
|
||||
$t('subscribers.query') }}
|
||||
</b-button>
|
||||
<b-button @click.prevent="toggleAdvancedSearch" icon-left="cancel" data-cy="btn-query-reset">
|
||||
{{ $t('subscribers.reset') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
@ -72,139 +69,122 @@
|
|||
</section><!-- control -->
|
||||
|
||||
<br />
|
||||
<b-table
|
||||
:data="subscribers.results"
|
||||
:loading="loading.subscribers"
|
||||
@check-all="onTableCheck" @check="onTableCheck"
|
||||
:checked-rows.sync="bulk.checked"
|
||||
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
||||
:current-page="queryParams.page" :per-page="subscribers.perPage" :total="subscribers.total"
|
||||
hoverable checkable backend-sorting @sort="onSort">
|
||||
|
||||
<template #top-left>
|
||||
<div class="actions">
|
||||
<a class="a" href='' @click.prevent="exportSubscribers"
|
||||
data-cy="btn-export-subscribers">
|
||||
<b-icon icon="cloud-download-outline" size="is-small" />
|
||||
{{ $t('subscribers.export') }}
|
||||
<b-table :data="subscribers.results" :loading="loading.subscribers" @check-all="onTableCheck" @check="onTableCheck"
|
||||
:checked-rows.sync="bulk.checked" paginated backend-pagination pagination-position="both"
|
||||
@page-change="onPageChange" :current-page="queryParams.page" :per-page="subscribers.perPage"
|
||||
:total="subscribers.total" hoverable checkable backend-sorting @sort="onSort">
|
||||
<template #top-left>
|
||||
<div class="actions">
|
||||
<a class="a" href="#" @click.prevent="exportSubscribers" data-cy="btn-export-subscribers">
|
||||
<b-icon icon="cloud-download-outline" size="is-small" />
|
||||
{{ $t('subscribers.export') }}
|
||||
</a>
|
||||
<template v-if="bulk.checked.length > 0">
|
||||
<a class="a" href="#" @click.prevent="showBulkListForm" data-cy="btn-manage-lists">
|
||||
<b-icon icon="format-list-bulleted-square" size="is-small" /> Manage lists
|
||||
</a>
|
||||
<template v-if="bulk.checked.length > 0">
|
||||
<a class="a" href='' @click.prevent="showBulkListForm" data-cy="btn-manage-lists">
|
||||
<b-icon icon="format-list-bulleted-square" size="is-small" /> Manage lists
|
||||
</a>
|
||||
<a class="a" href='' @click.prevent="deleteSubscribers"
|
||||
data-cy="btn-delete-subscribers">
|
||||
<b-icon icon="trash-can-outline" size="is-small" /> Delete
|
||||
</a>
|
||||
<a class="a" href='' @click.prevent="blocklistSubscribers"
|
||||
data-cy="btn-manage-blocklist">
|
||||
<b-icon icon="account-off-outline" size="is-small" /> Blocklist
|
||||
</a>
|
||||
<span class="a">
|
||||
{{ $t('subscribers.numSelected', { num: numSelectedSubscribers }) }}
|
||||
<span v-if="!bulk.all && subscribers.total > subscribers.perPage">
|
||||
—
|
||||
<a href="" @click.prevent="selectAllSubscribers">
|
||||
{{ $t('subscribers.selectAll', { num: subscribers.total }) }}
|
||||
</a>
|
||||
</span>
|
||||
<a class="a" href="#" @click.prevent="deleteSubscribers" data-cy="btn-delete-subscribers">
|
||||
<b-icon icon="trash-can-outline" size="is-small" /> Delete
|
||||
</a>
|
||||
<a class="a" href="#" @click.prevent="blocklistSubscribers" data-cy="btn-manage-blocklist">
|
||||
<b-icon icon="account-off-outline" size="is-small" /> Blocklist
|
||||
</a>
|
||||
<span class="a">
|
||||
{{ $t('subscribers.numSelected', { num: numSelectedSubscribers }) }}
|
||||
<span v-if="!bulk.all && subscribers.total > subscribers.perPage">
|
||||
—
|
||||
<a href="#" @click.prevent="selectAllSubscribers">
|
||||
{{ $t('subscribers.selectAll', { num: subscribers.total }) }}
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<b-table-column v-slot="props" field="status" :label="$t('globals.fields.status')"
|
||||
header-class="cy-status" :td-attrs="$utils.tdID" sortable>
|
||||
<a :href="`/subscribers/${props.row.id}`"
|
||||
@click.prevent="showEditForm(props.row)">
|
||||
<b-tag :class="props.row.status">
|
||||
{{ $t(`subscribers.status.${props.row.status}`) }}
|
||||
</b-tag>
|
||||
<b-table-column v-slot="props" field="status" :label="$t('globals.fields.status')" header-class="cy-status"
|
||||
:td-attrs="$utils.tdID" sortable>
|
||||
<a :href="`/subscribers/${props.row.id}`" @click.prevent="showEditForm(props.row)">
|
||||
<b-tag :class="props.row.status">
|
||||
{{ $t(`subscribers.status.${props.row.status}`) }}
|
||||
</b-tag>
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="email" :label="$t('subscribers.email')" header-class="cy-email" sortable>
|
||||
<a :href="`/subscribers/${props.row.id}`" @click.prevent="showEditForm(props.row)">
|
||||
{{ props.row.email }}
|
||||
</a>
|
||||
<b-taglist>
|
||||
<template v-for="l in props.row.lists">
|
||||
<router-link :to="`/subscribers/lists/${l.id}`" :key="l.id" style="padding-right:0.5em;">
|
||||
<b-tag :class="l.subscriptionStatus" size="is-small" :key="l.id">
|
||||
{{ l.name }}
|
||||
<sup v-if="l.optin === 'double' || l.subscriptionStatus == 'unsubscribed'">
|
||||
{{ $t(`subscribers.status.${l.subscriptionStatus}`) }}
|
||||
</sup>
|
||||
</b-tag>
|
||||
</router-link>
|
||||
</template>
|
||||
</b-taglist>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" header-class="cy-name" sortable>
|
||||
<a :href="`/subscribers/${props.row.id}`" @click.prevent="showEditForm(props.row)">
|
||||
{{ props.row.name }}
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="lists" :label="$t('globals.terms.lists')" header-class="cy-lists" centered>
|
||||
{{ listCount(props.row.lists) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')"
|
||||
header-class="cy-created_at" sortable>
|
||||
{{ $utils.niceDate(props.row.createdAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="updated_at" :label="$t('globals.fields.updatedAt')"
|
||||
header-class="cy-updated_at" sortable>
|
||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="actions" align="right">
|
||||
<div>
|
||||
<a :href="`/api/subscribers/${props.row.id}/export`" data-cy="btn-download"
|
||||
:aria-label="$t('subscribers.downloadData')">
|
||||
<b-tooltip :label="$t('subscribers.downloadData')" type="is-dark">
|
||||
<b-icon icon="cloud-download-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="email" :label="$t('subscribers.email')"
|
||||
header-class="cy-email" sortable>
|
||||
<a :href="`/subscribers/${props.row.id}`"
|
||||
@click.prevent="showEditForm(props.row)">
|
||||
{{ props.row.email }}
|
||||
<a :href="`/subscribers/${props.row.id}`" @click.prevent="showEditForm(props.row)" data-cy="btn-edit"
|
||||
:aria-label="$t('globals.buttons.edit')">
|
||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||
<b-icon icon="pencil-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<b-taglist>
|
||||
<template v-for="l in props.row.lists">
|
||||
<router-link :to="`/subscribers/lists/${l.id}`"
|
||||
v-bind:key="l.id" style="padding-right:0.5em;">
|
||||
<b-tag :class="l.subscriptionStatus" size="is-small" :key="l.id">
|
||||
{{ l.name }}
|
||||
<sup v-if="l.optin === 'double' || l.subscriptionStatus == 'unsubscribed'">
|
||||
{{ $t(`subscribers.status.${l.subscriptionStatus}`) }}
|
||||
</sup>
|
||||
</b-tag>
|
||||
</router-link>
|
||||
</template>
|
||||
</b-taglist>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')"
|
||||
header-class="cy-name" sortable>
|
||||
<a :href="`/subscribers/${props.row.id}`"
|
||||
@click.prevent="showEditForm(props.row)">
|
||||
{{ props.row.name }}
|
||||
<a href="#" @click.prevent="deleteSubscriber(props.row)" data-cy="btn-delete"
|
||||
:aria-label="$t('globals.buttons.delete')">
|
||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
</b-table-column>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="lists" :label="$t('globals.terms.lists')"
|
||||
header-class="cy-lists" centered>
|
||||
{{ listCount(props.row.lists) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')"
|
||||
header-class="cy-created_at" sortable>
|
||||
{{ $utils.niceDate(props.row.createdAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="updated_at" :label="$t('globals.fields.updatedAt')"
|
||||
header-class="cy-updated_at" sortable>
|
||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="actions" align="right">
|
||||
<div>
|
||||
<a :href="`/api/subscribers/${props.row.id}/export`" data-cy="btn-download">
|
||||
<b-tooltip :label="$t('subscribers.downloadData')" type="is-dark">
|
||||
<b-icon icon="cloud-download-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a :href="`/subscribers/${props.row.id}`"
|
||||
@click.prevent="showEditForm(props.row)" data-cy="btn-edit">
|
||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||
<b-icon icon="pencil-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a href='' @click.prevent="deleteSubscriber(props.row)" data-cy="btn-delete">
|
||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<template #empty v-if="!loading.subscribers">
|
||||
<empty-placeholder />
|
||||
</template>
|
||||
<template #empty v-if="!loading.subscribers">
|
||||
<empty-placeholder />
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<!-- Manage list modal -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isBulkListFormVisible"
|
||||
:width="500" class="has-overflow">
|
||||
<subscriber-bulk-list :numSubscribers="this.numSelectedSubscribers"
|
||||
@finished="bulkChangeLists" />
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isBulkListFormVisible" :width="500" class="has-overflow">
|
||||
<subscriber-bulk-list :num-subscribers="this.numSelectedSubscribers" @finished="bulkChangeLists" />
|
||||
</b-modal>
|
||||
|
||||
<!-- Add / edit form modal -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="800"
|
||||
@close="onFormClose">
|
||||
<subscriber-form :data="curItem" :isEditing="isEditing"
|
||||
@finished="querySubscribers"></subscriber-form>
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="800" @close="onFormClose">
|
||||
<subscriber-form :data="curItem" :is-editing="isEditing" @finished="querySubscribers" />
|
||||
</b-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -212,10 +192,10 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import SubscriberForm from './SubscriberForm.vue';
|
||||
import SubscriberBulkList from './SubscriberBulkList.vue';
|
||||
import EmptyPlaceholder from '../components/EmptyPlaceholder.vue';
|
||||
import { uris } from '../constants';
|
||||
import SubscriberBulkList from './SubscriberBulkList.vue';
|
||||
import SubscriberForm from './SubscriberForm.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
@ -445,8 +425,10 @@ export default Vue.extend({
|
|||
}).then(() => {
|
||||
this.querySubscribers();
|
||||
|
||||
this.$utils.toast(this.$t('subscribers.subscribersDeleted',
|
||||
{ num: this.numSelectedSubscribers }));
|
||||
this.$utils.toast(this.$t(
|
||||
'subscribers.subscribersDeleted',
|
||||
{ num: this.numSelectedSubscribers },
|
||||
));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,31 +3,37 @@
|
|||
<form @submit.prevent="onSubmit">
|
||||
<div class="modal-card content template-modal-content" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<b-button @click="previewTemplate"
|
||||
class="is-pulled-right" type="is-primary"
|
||||
icon-left="file-find-outline">{{ $t('templates.preview') }}</b-button>
|
||||
<b-button @click="previewTemplate" class="is-pulled-right" type="is-primary" icon-left="file-find-outline">
|
||||
{{ $t('templates.preview') }}
|
||||
</b-button>
|
||||
|
||||
<template v-if="isEditing">
|
||||
<h4>{{ data.name }}</h4>
|
||||
<p class="has-text-grey is-size-7">
|
||||
{{ $t('globals.fields.id') }}: <span data-cy="id">{{ data.id }}</span>
|
||||
</p>
|
||||
</template>
|
||||
<h4 v-else>{{ $t('templates.newTemplate') }}</h4>
|
||||
<template v-if="isEditing">
|
||||
<h4>{{ data.name }}</h4>
|
||||
<p class="has-text-grey is-size-7">
|
||||
{{ $t('globals.fields.id') }}: <span data-cy="id">{{ data.id }}</span>
|
||||
</p>
|
||||
</template>
|
||||
<h4 v-else>
|
||||
{{ $t('templates.newTemplate') }}
|
||||
</h4>
|
||||
</header>
|
||||
<section expanded class="modal-card-body">
|
||||
<div class="columns">
|
||||
<div class="column is-9">
|
||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" name="name"
|
||||
:placeholder="$t('globals.fields.name')" required />
|
||||
:placeholder="$t('globals.fields.name')" required />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('globals.fields.type')" label-position="on-border">
|
||||
<b-select v-model="form.type" :disabled="isEditing" expanded>
|
||||
<option value="campaign">{{ $tc('globals.terms.campaign') }}</option>
|
||||
<option value="tx">{{ $tc('globals.terms.tx') }}</option>
|
||||
<option value="campaign">
|
||||
{{ $tc('globals.terms.campaign') }}
|
||||
</option>
|
||||
<option value="tx">
|
||||
{{ $tc('globals.terms.tx') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -41,8 +47,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<b-field v-if="form.body !== null"
|
||||
:label="$t('templates.rawHTML')" label-position="on-border">
|
||||
<b-field v-if="form.body !== null" :label="$t('templates.rawHTML')" label-position="on-border">
|
||||
<html-editor v-model="form.body" name="body" />
|
||||
</b-field>
|
||||
|
||||
|
@ -50,24 +55,23 @@
|
|||
<template v-if="form.type === 'campaign'">
|
||||
{{ $t('templates.placeholderHelp', { placeholder: egPlaceholder }) }}
|
||||
</template>
|
||||
<a target="_blank" href="https://listmonk.app/docs/templating">
|
||||
<a target="_blank" rel="noopener noreferer" href="https://listmonk.app/docs/templating">
|
||||
{{ $t('globals.buttons.learnMore') }}
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="$parent.close()">{{ $t('globals.buttons.close') }}</b-button>
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
:loading="loading.templates">{{ $t('globals.buttons.save') }}</b-button>
|
||||
<b-button @click="$parent.close()">
|
||||
{{ $t('globals.buttons.close') }}
|
||||
</b-button>
|
||||
<b-button native-type="submit" type="is-primary" :loading="loading.templates">
|
||||
{{ $t('globals.buttons.save') }}
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
<campaign-preview v-if="previewItem"
|
||||
type='template'
|
||||
:title="previewItem.name"
|
||||
:templateType="previewItem.type"
|
||||
:body="form.body"
|
||||
@close="closePreview"></campaign-preview>
|
||||
<campaign-preview v-if="previewItem" type="template" :title="previewItem.name" :template-type="previewItem.type"
|
||||
:body="form.body" @close="closePreview" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
@ -84,8 +88,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
props: {
|
||||
data: Object,
|
||||
isEditing: null,
|
||||
data: { type: Object, default: () => { } },
|
||||
isEditing: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -2,41 +2,39 @@
|
|||
<section class="templates">
|
||||
<header class="columns page-header">
|
||||
<div class="column is-10">
|
||||
<h1 class="title is-4">{{ $t('globals.terms.templates') }}
|
||||
<span v-if="templates.length > 0">({{ templates.length }})</span></h1>
|
||||
<h1 class="title is-4">
|
||||
{{ $t('globals.terms.templates') }}
|
||||
<span v-if="templates.length > 0">({{ templates.length }})</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<b-field expanded>
|
||||
<b-button expanded type="is-primary" icon-left="plus" class="btn-new"
|
||||
@click="showNewForm">
|
||||
<b-button expanded type="is-primary" icon-left="plus" class="btn-new" @click="showNewForm">
|
||||
{{ $t('globals.buttons.new') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<b-table :data="templates" :hoverable="true" :loading="loading.templates"
|
||||
default-sort="createdAt">
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')"
|
||||
:td-attrs="$utils.tdID" sortable>
|
||||
<b-table :data="templates" :hoverable="true" :loading="loading.templates" default-sort="createdAt">
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" :td-attrs="$utils.tdID" sortable>
|
||||
<a href="#" @click.prevent="showEditForm(props.row)">
|
||||
{{ props.row.name }}
|
||||
</a>
|
||||
<b-tag v-if="props.row.isDefault">{{ $t('templates.default') }}</b-tag>
|
||||
<b-tag v-if="props.row.isDefault">
|
||||
{{ $t('templates.default') }}
|
||||
</b-tag>
|
||||
|
||||
<p class="is-size-7 has-text-grey" v-if="props.row.type === 'tx'">
|
||||
{{ props.row.subject }}
|
||||
</p>
|
||||
</p>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="type"
|
||||
:label="$t('globals.fields.type')" sortable>
|
||||
<b-tag v-if="props.row.type === 'campaign'"
|
||||
:class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||
<b-table-column v-slot="props" field="type" :label="$t('globals.fields.type')" sortable>
|
||||
<b-tag v-if="props.row.type === 'campaign'" :class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||
{{ $tc('globals.terms.campaign', 1) }}
|
||||
</b-tag>
|
||||
<b-tag v-else
|
||||
:class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||
<b-tag v-else :class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||
{{ $tc('globals.terms.tx', 1) }}
|
||||
</b-tag>
|
||||
</b-table-column>
|
||||
|
@ -45,56 +43,54 @@
|
|||
{{ props.row.id }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="createdAt"
|
||||
:label="$t('globals.fields.createdAt')" sortable>
|
||||
<b-table-column v-slot="props" field="createdAt" :label="$t('globals.fields.createdAt')" sortable>
|
||||
{{ $utils.niceDate(props.row.createdAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="updatedAt"
|
||||
:label="$t('globals.fields.updatedAt')" sortable>
|
||||
<b-table-column v-slot="props" field="updatedAt" :label="$t('globals.fields.updatedAt')" sortable>
|
||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" cell-class="actions" align="right">
|
||||
<div>
|
||||
<a href="#" @click.prevent="previewTemplate(props.row)" data-cy="btn-preview">
|
||||
<a href="#" @click.prevent="previewTemplate(props.row)" data-cy="btn-preview"
|
||||
:aria-label="$t('templates.preview')">
|
||||
<b-tooltip :label="$t('templates.preview')" type="is-dark">
|
||||
<b-icon icon="file-find-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a href="#" @click.prevent="showEditForm(props.row)" data-cy="btn-edit">
|
||||
<a href="#" @click.prevent="showEditForm(props.row)" data-cy="btn-edit"
|
||||
:aria-label="$t('globals.buttons.edit')">
|
||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||
<b-icon icon="pencil-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a href="" @click.prevent="$utils.prompt(`Clone template`,
|
||||
{ placeholder: 'Name', value: `Copy of ${props.row.name}`},
|
||||
(name) => cloneTemplate(name, props.row))"
|
||||
data-cy="btn-clone">
|
||||
<a href="#" @click.prevent="$utils.prompt(`Clone template`,
|
||||
{ placeholder: 'Name', value: `Copy of ${props.row.name}` },
|
||||
(name) => cloneTemplate(name, props.row))" data-cy="btn-clone" :aria-label="$t('globals.buttons.clone')">
|
||||
<b-tooltip :label="$t('globals.buttons.clone')" type="is-dark">
|
||||
<b-icon icon="file-multiple-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<a v-if="!props.row.isDefault && props.row.type !== 'tx'" href="#"
|
||||
@click.prevent="$utils.confirm(null, () => makeTemplateDefault(props.row))"
|
||||
data-cy="btn-set-default">
|
||||
@click.prevent="$utils.confirm(null, () => makeTemplateDefault(props.row))" data-cy="btn-set-default"
|
||||
:aria-label="$t('templates.makeDefault')">
|
||||
<b-tooltip :label="$t('templates.makeDefault')" type="is-dark">
|
||||
<b-icon icon="check-circle-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<span v-else class="a has-text-grey-light">
|
||||
<b-icon icon="check-circle-outline" size="is-small" />
|
||||
<b-icon icon="check-circle-outline" size="is-small" />
|
||||
</span>
|
||||
|
||||
<a v-if="!props.row.isDefault" href="#"
|
||||
@click.prevent="$utils.confirm(null, () => deleteTemplate(props.row))"
|
||||
data-cy="btn-delete">
|
||||
<a v-if="!props.row.isDefault" href="#" @click.prevent="$utils.confirm(null, () => deleteTemplate(props.row))"
|
||||
data-cy="btn-delete" :aria-label="$t('globals.buttons.delete')">
|
||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</b-tooltip>
|
||||
</a>
|
||||
<span v-else class="a has-text-grey-light">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
</span>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
@ -105,27 +101,22 @@
|
|||
</b-table>
|
||||
|
||||
<!-- Add / edit form modal -->
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible"
|
||||
:width="1200" :can-cancel="false" class="template-modal">
|
||||
<template-form :data="curItem" :isEditing="isEditing"
|
||||
@finished="formFinished"></template-form>
|
||||
<b-modal scroll="keep" :aria-modal="true" :active.sync="isFormVisible" :width="1200" :can-cancel="false"
|
||||
class="template-modal">
|
||||
<template-form :data="curItem" :is-editing="isEditing" @finished="formFinished" />
|
||||
</b-modal>
|
||||
|
||||
<campaign-preview v-if="previewItem"
|
||||
type='template'
|
||||
:id="previewItem.id"
|
||||
:templateType="previewItem.type"
|
||||
:title="previewItem.name"
|
||||
@close="closePreview"></campaign-preview>
|
||||
<campaign-preview v-if="previewItem" type="template" :id="previewItem.id" :template-type="previewItem.type"
|
||||
:title="previewItem.name" @close="closePreview" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import TemplateForm from './TemplateForm.vue';
|
||||
import CampaignPreview from '../components/CampaignPreview.vue';
|
||||
import EmptyPlaceholder from '../components/EmptyPlaceholder.vue';
|
||||
import TemplateForm from './TemplateForm.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
</div>
|
||||
|
||||
<b-field :label="$t('settings.appearance.customCSS')" label-position="on-border">
|
||||
<html-editor v-model="data['appearance.admin.custom_css']" name="body"
|
||||
language="css" />
|
||||
<html-editor v-model="data['appearance.admin.custom_css']" name="body" language="css" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.appearance.customJS')" label-position="on-border">
|
||||
<html-editor v-model="data['appearance.admin.custom_js']" name="body"
|
||||
language="css" />
|
||||
<html-editor v-model="data['appearance.admin.custom_js']" name="body" language="css" />
|
||||
</b-field>
|
||||
</b-tab-item><!-- admin -->
|
||||
|
||||
|
@ -23,13 +21,11 @@
|
|||
</div>
|
||||
|
||||
<b-field :label="$t('settings.appearance.customCSS')" label-position="on-border">
|
||||
<html-editor v-model="data['appearance.public.custom_css']" name="body"
|
||||
language="css" />
|
||||
<html-editor v-model="data['appearance.public.custom_css']" name="body" language="css" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.appearance.customJS')" label-position="on-border">
|
||||
<html-editor v-model="data['appearance.public.custom_js']" name="body"
|
||||
language="js" />
|
||||
<html-editor v-model="data['appearance.public.custom_js']" name="body" language="js" />
|
||||
</b-field>
|
||||
</b-tab-item><!-- public -->
|
||||
</b-tabs>
|
||||
|
@ -48,7 +44,7 @@ export default Vue.extend({
|
|||
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -8,26 +8,32 @@
|
|||
</div>
|
||||
<div class="column">
|
||||
<div v-for="typ in bounceTypes" :key="typ" class="columns">
|
||||
<div class="column is-2" :class="{'disabled': !data['bounce.enabled']}"
|
||||
:label="$t('settings.bounces.count')" label-position="on-border">
|
||||
{{ $t(`bounces.${typ}`) }}
|
||||
<div class="column is-2" :class="{ disabled: !data['bounce.enabled'] }" :label="$t('settings.bounces.count')"
|
||||
label-position="on-border">
|
||||
{{ $t(`bounces.${typ}`) }}
|
||||
</div>
|
||||
<div class="column is-4" :class="{'disabled': !data['bounce.enabled']}">
|
||||
<div class="column is-4" :class="{ disabled: !data['bounce.enabled'] }">
|
||||
<b-field :label="$t('settings.bounces.count')" label-position="on-border"
|
||||
:message="$t('settings.bounces.countHelp')" data-cy="btn-bounce-count">
|
||||
<b-numberinput v-model="data['bounce.actions'][typ]['count']"
|
||||
name="bounce.count" type="is-light"
|
||||
<b-numberinput v-model="data['bounce.actions'][typ]['count']" name="bounce.count" type="is-light"
|
||||
controls-position="compact" placeholder="3" min="1" max="1000" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4" :class="{'disabled': !data['bounce.enabled']}">
|
||||
<div class="column is-4" :class="{ disabled: !data['bounce.enabled'] }">
|
||||
<b-field :label="$t('settings.bounces.action')" label-position="on-border">
|
||||
<b-select name="bounce.action" v-model="data['bounce.actions'][typ]['action']"
|
||||
expanded>
|
||||
<option value="none">{{ $t('globals.terms.none') }}</option>
|
||||
<option value="unsubscribe">{{ $t('email.unsub') }}</option>
|
||||
<option value="blocklist">{{ $t('settings.bounces.blocklist') }}</option>
|
||||
<option value="delete">{{ $t('globals.buttons.delete') }}</option>
|
||||
<b-select name="bounce.action" v-model="data['bounce.actions'][typ]['action']" expanded>
|
||||
<option value="none">
|
||||
{{ $t('globals.terms.none') }}
|
||||
</option>
|
||||
<option value="unsubscribe">
|
||||
{{ $t('email.unsub') }}
|
||||
</option>
|
||||
<option value="blocklist">
|
||||
{{ $t('settings.bounces.blocklist') }}
|
||||
</option>
|
||||
<option value="delete">
|
||||
{{ $t('globals.buttons.delete') }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -36,141 +42,131 @@
|
|||
</div><!-- columns -->
|
||||
|
||||
<div class="mb-6">
|
||||
<b-field :label="$t('settings.bounces.enableWebhooks')"
|
||||
data-cy="btn-enable-bounce-webhook">
|
||||
<b-switch v-model="data['bounce.webhooks_enabled']"
|
||||
:disabled="!data['bounce.enabled']"
|
||||
name="webhooks_enabled" :native-value="true"
|
||||
data-cy="btn-enable-bounce-webhook" />
|
||||
<b-field :label="$t('settings.bounces.enableWebhooks')" data-cy="btn-enable-bounce-webhook">
|
||||
<b-switch v-model="data['bounce.webhooks_enabled']" :disabled="!data['bounce.enabled']" name="webhooks_enabled"
|
||||
:native-value="true" data-cy="btn-enable-bounce-webhook" />
|
||||
<p class="has-text-grey">
|
||||
<a href="https://listmonk.app/docs/bounces" target="_blank">{{ $t('globals.buttons.learnMore') }} →</a>
|
||||
<a href="https://listmonk.app/docs/bounces" target="_blank" rel="noopener noreferer">{{
|
||||
$t('globals.buttons.learnMore') }} →</a>
|
||||
</p>
|
||||
</b-field>
|
||||
<div class="box" v-if="data['bounce.webhooks_enabled']">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.enableSES')">
|
||||
<b-switch v-model="data['bounce.ses_enabled']"
|
||||
name="ses_enabled" :native-value="true" data-cy="btn-enable-bounce-ses" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.enableSES')">
|
||||
<b-switch v-model="data['bounce.ses_enabled']" name="ses_enabled" :native-value="true"
|
||||
data-cy="btn-enable-bounce-ses" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.bounces.enableSendgrid')">
|
||||
<b-switch v-model="data['bounce.sendgrid_enabled']"
|
||||
name="sendgrid_enabled" :native-value="true"
|
||||
data-cy="btn-enable-bounce-sendgrid" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.sendgridKey')"
|
||||
:message="$t('globals.messages.passwordChange')">
|
||||
<b-input v-model="data['bounce.sendgrid_key']" type="password"
|
||||
:disabled="!data['bounce.sendgrid_enabled']"
|
||||
name="sendgrid_enabled" :native-value="true"
|
||||
data-cy="btn-enable-bounce-sendgrid" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.bounces.enableSendgrid')">
|
||||
<b-switch v-model="data['bounce.sendgrid_enabled']" name="sendgrid_enabled" :native-value="true"
|
||||
data-cy="btn-enable-bounce-sendgrid" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.bounces.enablePostmark')">
|
||||
<b-switch v-model="data['bounce.postmark'].enabled"
|
||||
name="postmark_enabled" :native-value="true"
|
||||
data-cy="btn-enable-bounce-postmark" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.postmarkUsername')"
|
||||
:message="$t('settings.bounces.postmarkUsernameHelp')">
|
||||
<b-input v-model="data['bounce.postmark'].username" type="text"
|
||||
:disabled="!data['bounce.postmark'].enabled"
|
||||
name="postmark_username"
|
||||
data-cy="btn-enable-bounce-postmark" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.postmarkPassword')"
|
||||
:message="$t('globals.messages.passwordChange')">
|
||||
<b-input v-model="data['bounce.postmark'].password" type="password"
|
||||
:disabled="!data['bounce.postmark'].enabled"
|
||||
name="postmark_password"
|
||||
data-cy="btn-enable-bounce-postmark" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.sendgridKey')" :message="$t('globals.messages.passwordChange')">
|
||||
<b-input v-model="data['bounce.sendgrid_key']" type="password" :disabled="!data['bounce.sendgrid_enabled']"
|
||||
name="sendgrid_enabled" :native-value="true" data-cy="btn-enable-bounce-sendgrid" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.bounces.enablePostmark')">
|
||||
<b-switch v-model="data['bounce.postmark'].enabled" name="postmark_enabled" :native-value="true"
|
||||
data-cy="btn-enable-bounce-postmark" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.postmarkUsername')"
|
||||
:message="$t('settings.bounces.postmarkUsernameHelp')">
|
||||
<b-input v-model="data['bounce.postmark'].username" type="text" :disabled="!data['bounce.postmark'].enabled"
|
||||
name="postmark_username" data-cy="btn-enable-bounce-postmark" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.bounces.postmarkPassword')" :message="$t('globals.messages.passwordChange')">
|
||||
<b-input v-model="data['bounce.postmark'].password" type="password"
|
||||
:disabled="!data['bounce.postmark'].enabled" name="postmark_password"
|
||||
data-cy="btn-enable-bounce-postmark" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- bounce mailbox -->
|
||||
<b-field :label="$t('settings.bounces.enableMailbox')">
|
||||
<b-switch v-if="data['bounce.mailboxes']"
|
||||
v-model="data['bounce.mailboxes'][0].enabled"
|
||||
:disabled="!data['bounce.enabled']"
|
||||
name="enabled" :native-value="true" data-cy="btn-enable-bounce-mailbox" />
|
||||
<b-switch v-if="data['bounce.mailboxes']" v-model="data['bounce.mailboxes'][0].enabled"
|
||||
:disabled="!data['bounce.enabled']" name="enabled" :native-value="true" data-cy="btn-enable-bounce-mailbox" />
|
||||
</b-field>
|
||||
|
||||
<template v-if="data['bounce.enabled'] && data['bounce.mailboxes'][0].enabled">
|
||||
<div class="block box" v-for="(item, n) in data['bounce.mailboxes']" :key="n">
|
||||
<div class="columns">
|
||||
<div class="column" :class="{'disabled': !item.enabled}">
|
||||
<div class="column" :class="{ disabled: !item.enabled }">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.bounces.type')" label-position="on-border">
|
||||
<b-select v-model="item.type" name="type">
|
||||
<option value="pop">POP</option>
|
||||
<option value="pop">
|
||||
POP
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('settings.mailserver.host')" label-position="on-border"
|
||||
:message="$t('settings.mailserver.hostHelp')">
|
||||
<b-input v-model="item.host" name="host"
|
||||
placeholder='bounce.yourmailserver.net' :maxlength="200" />
|
||||
<b-input v-model="item.host" name="host" placeholder="bounce.yourmailserver.net" :maxlength="200" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.mailserver.port')" label-position="on-border"
|
||||
:message="$t('settings.mailserver.portHelp')">
|
||||
<b-numberinput v-model="item.port" name="port" type="is-light"
|
||||
controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
<b-numberinput v-model="item.port" name="port" type="is-light" controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div><!-- host -->
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.mailserver.authProtocol')"
|
||||
label-position="on-border">
|
||||
<b-field :label="$t('settings.mailserver.authProtocol')" label-position="on-border">
|
||||
<b-select v-model="item.auth_protocol" name="auth_protocol">
|
||||
<option value="none">none</option>
|
||||
<option v-if="item.type === 'pop'" value="userpass">userpass</option>
|
||||
<option value="none">
|
||||
none
|
||||
</option>
|
||||
<option v-if="item.type === 'pop'" value="userpass">
|
||||
userpass
|
||||
</option>
|
||||
<template v-else>
|
||||
<option value="cram">cram</option>
|
||||
<option value="plain">plain</option>
|
||||
<option value="login">login</option>
|
||||
<option value="cram">
|
||||
cram
|
||||
</option>
|
||||
<option value="plain">
|
||||
plain
|
||||
</option>
|
||||
<option value="login">
|
||||
login
|
||||
</option>
|
||||
</template>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field :label="$t('settings.mailserver.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 :label="$t('settings.mailserver.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="$t('settings.mailserver.password')"
|
||||
label-position="on-border" expanded
|
||||
<b-field :label="$t('settings.mailserver.password')" label-position="on-border" expanded
|
||||
:message="$t('settings.mailserver.passwordHelp')">
|
||||
<b-input v-model="item.password"
|
||||
:disabled="item.auth_protocol === 'none'"
|
||||
name="password" type="password"
|
||||
:placeholder="$t('settings.mailserver.passwordHelp')"
|
||||
:maxlength="200" />
|
||||
<b-input v-model="item.password" :disabled="item.auth_protocol === 'none'" name="password"
|
||||
type="password" :placeholder="$t('settings.mailserver.passwordHelp')" :maxlength="200" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -179,24 +175,21 @@
|
|||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field grouped>
|
||||
<b-field :label="$t('settings.mailserver.tls')" expanded
|
||||
:message="$t('settings.mailserver.tlsHelp')">
|
||||
<b-field :label="$t('settings.mailserver.tls')" expanded :message="$t('settings.mailserver.tlsHelp')">
|
||||
<b-switch v-model="item.tls_enabled" name="item.tls_enabled" />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.mailserver.skipTLS')" expanded
|
||||
:message="$t('settings.mailserver.skipTLSHelp')">
|
||||
<b-switch v-model="item.tls_skip_verify"
|
||||
:disabled="!item.tls_enabled" name="item.tls_skip_verify" />
|
||||
<b-switch v-model="item.tls_skip_verify" :disabled="!item.tls_enabled" name="item.tls_skip_verify" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column"></div>
|
||||
<div class="column" />
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.bounces.scanInterval')" expanded
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.bounces.scanInterval')" expanded label-position="on-border"
|
||||
:message="$t('settings.bounces.scanIntervalHelp')">
|
||||
<b-input v-model="item.scan_interval" name="scan_interval"
|
||||
placeholder="15m" :pattern="regDuration" :maxlength="10" />
|
||||
<b-input v-model="item.scan_interval" name="scan_interval" placeholder="15m" :pattern="regDuration"
|
||||
:maxlength="10" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div><!-- TLS -->
|
||||
|
@ -214,7 +207,7 @@ import { regDuration } from '../../constants';
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
<template>
|
||||
<div class="items">
|
||||
<b-field :label="$t('settings.general.siteName')" label-position="on-border">
|
||||
<b-input v-model="data['app.site_name']" name="app.site_name"
|
||||
:label="$t('settings.general.siteName')" :maxlength="300" required />
|
||||
<b-input v-model="data['app.site_name']" name="app.site_name" :label="$t('settings.general.siteName')"
|
||||
:maxlength="300" required />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.general.rootURL')" label-position="on-border"
|
||||
:message="$t('settings.general.rootURLHelp')">
|
||||
<b-input v-model="data['app.root_url']" name="app.root_url"
|
||||
placeholder='https://listmonk.yoursite.com' :maxlength="300" required />
|
||||
<b-input v-model="data['app.root_url']" name="app.root_url" placeholder="https://listmonk.yoursite.com"
|
||||
:maxlength="300" required />
|
||||
</b-field>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('settings.general.logoURL')" label-position="on-border"
|
||||
:message="$t('settings.general.logoURLHelp')">
|
||||
<b-input v-model="data['app.logo_url']" name="app.logo_url"
|
||||
placeholder='https://listmonk.yoursite.com/logo.png' :maxlength="300" />
|
||||
<b-input v-model="data['app.logo_url']" name="app.logo_url" placeholder="https://listmonk.yoursite.com/logo.png"
|
||||
:maxlength="300" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<b-field :label="$t('settings.general.faviconURL')" label-position="on-border"
|
||||
:message="$t('settings.general.faviconURLHelp')">
|
||||
<b-input v-model="data['app.favicon_url']" name="app.favicon_url"
|
||||
placeholder='https://listmonk.yoursite.com/favicon.png' :maxlength="300" />
|
||||
placeholder="https://listmonk.yoursite.com/favicon.png" :maxlength="300" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,33 +32,31 @@
|
|||
<b-field :label="$t('settings.general.fromEmail')" label-position="on-border"
|
||||
:message="$t('settings.general.fromEmailHelp')">
|
||||
<b-input v-model="data['app.from_email']" name="app.from_email"
|
||||
placeholder='Listmonk <noreply@listmonk.yoursite.com>'
|
||||
pattern="(.+?)\s<(.+?)@(.+?)>" :maxlength="300" />
|
||||
placeholder="Listmonk <noreply@listmonk.yoursite.com>" pattern="(.+?)\s<(.+?)@(.+?)>" :maxlength="300" />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.general.adminNotifEmails')" label-position="on-border"
|
||||
:message="$t('settings.general.adminNotifEmailsHelp')">
|
||||
<b-taginput v-model="data['app.notify_emails']" name="app.notify_emails"
|
||||
:before-adding="(v) => v.match(/(.+?)@(.+?)/)"
|
||||
placeholder='you@yoursite.com' />
|
||||
:before-adding="(v) => v.match(/(.+?)@(.+?)/)" placeholder="you@yoursite.com" />
|
||||
</b-field>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h2 class="is-size-4 mb-5">{{ $tc('globals.terms.subscriptions', 2) }}</h2>
|
||||
<h2 class="is-size-4 mb-5">
|
||||
{{ $tc('globals.terms.subscriptions', 2) }}
|
||||
</h2>
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.general.enablePublicSubPage')"
|
||||
:message="$t('settings.general.enablePublicSubPageHelp')">
|
||||
<b-switch v-model="data['app.enable_public_subscription_page']"
|
||||
name="app.enable_public_subscription_page" />
|
||||
<b-switch v-model="data['app.enable_public_subscription_page']" name="app.enable_public_subscription_page" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.general.sendOptinConfirm')"
|
||||
:message="$t('settings.general.sendOptinConfirmHelp')">
|
||||
<b-switch v-model="data['app.send_optin_confirmation']"
|
||||
name="app.send_optin_confirmation" />
|
||||
<b-switch v-model="data['app.send_optin_confirmation']" name="app.send_optin_confirmation" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,41 +64,41 @@
|
|||
<hr />
|
||||
|
||||
<div>
|
||||
<h2 class="is-size-4 mb-5">{{ $t('campaigns.archive') }}</h2>
|
||||
<h2 class="is-size-4 mb-5">
|
||||
{{ $t('campaigns.archive') }}
|
||||
</h2>
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.general.enablePublicArchive')"
|
||||
:message="$t('settings.general.enablePublicArchiveHelp')">
|
||||
<b-switch v-model="data['app.enable_public_archive']"
|
||||
name="app.enable_public_archive" />
|
||||
<b-switch v-model="data['app.enable_public_archive']" name="app.enable_public_archive" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.general.enablePublicArchiveRSSContent')"
|
||||
:message="$t('settings.general.enablePublicArchiveRSSContentHelp')">
|
||||
<b-switch v-model="data['app.enable_public_archive_rss_content']"
|
||||
name="app.enable_public_archive_rss_content" />
|
||||
name="app.enable_public_archive_rss_content" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<b-field :label="$t('settings.general.checkUpdates')"
|
||||
:message="$t('settings.general.checkUpdatesHelp')">
|
||||
<b-switch v-model="data['app.check_updates']"
|
||||
name="app.check_updates" />
|
||||
<b-field :label="$t('settings.general.checkUpdates')" :message="$t('settings.general.checkUpdatesHelp')">
|
||||
<b-switch v-model="data['app.check_updates']" name="app.check_updates" />
|
||||
</b-field>
|
||||
|
||||
<hr />
|
||||
<b-field :label="$t('settings.general.language')" label-position="on-border" :addons="false">
|
||||
<b-select v-model="data['app.lang']" name="app.lang">
|
||||
<option v-for="l in serverConfig.langs" :key="l.code" :value="l.code">
|
||||
{{ l.name }}
|
||||
</option>
|
||||
<option v-for="l in serverConfig.langs" :key="l.code" :value="l.code">
|
||||
{{ l.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
<p class="mt-2">
|
||||
<a href="https://listmonk.app/docs/i18n/#additional-language-packs" target="_blank">{{ $t('globals.buttons.more') }} →</a>
|
||||
<a href="https://listmonk.app/docs/i18n/#additional-language-packs" target="_blank" rel="noopener noreferer">{{
|
||||
$t('globals.buttons.more') }} →</a>
|
||||
</p>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -113,7 +111,7 @@ import { mapState } from 'vuex';
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -4,16 +4,19 @@
|
|||
<div class="column">
|
||||
<b-field :label="$t('settings.media.provider')" label-position="on-border">
|
||||
<b-select v-model="data['upload.provider']" name="upload.provider">
|
||||
<option value="filesystem">filesystem</option>
|
||||
<option value="s3">s3</option>
|
||||
<option value="filesystem">
|
||||
filesystem
|
||||
</option>
|
||||
<option value="s3">
|
||||
s3
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-10">
|
||||
<b-field :label="$t('settings.media.upload.extensions')" label-position="on-border"
|
||||
expanded>
|
||||
<b-taginput v-model="data['upload.extensions']" name="tags" ellipsis
|
||||
icon="tag-outline" placeholder="jpg, png, gif .."></b-taginput>
|
||||
<b-field :label="$t('settings.media.upload.extensions')" label-position="on-border" expanded>
|
||||
<b-taginput v-model="data['upload.extensions']" name="tags" ellipsis icon="tag-outline"
|
||||
placeholder="jpg, png, gif .." />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,42 +25,35 @@
|
|||
<div class="block" v-if="data['upload.provider'] === 'filesystem'">
|
||||
<b-field :label="$t('settings.media.upload.path')" label-position="on-border"
|
||||
:message="$t('settings.media.upload.pathHelp')">
|
||||
<b-input v-model="data['upload.filesystem.upload_path']"
|
||||
name="app.upload_path" placeholder='/home/listmonk/uploads'
|
||||
:maxlength="200" required />
|
||||
<b-input v-model="data['upload.filesystem.upload_path']" name="app.upload_path"
|
||||
placeholder="/home/listmonk/uploads" :maxlength="200" required />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.media.upload.uri')" label-position="on-border"
|
||||
:message="$t('settings.media.upload.uriHelp')">
|
||||
<b-input v-model="data['upload.filesystem.upload_uri']"
|
||||
name="app.upload_uri" placeholder='/uploads' :maxlength="200"
|
||||
required pattern="^\/(.+?)" />
|
||||
<b-input v-model="data['upload.filesystem.upload_uri']" name="app.upload_uri" placeholder="/uploads"
|
||||
:maxlength="200" required pattern="^\/(.+?)" />
|
||||
</b-field>
|
||||
</div><!-- filesystem -->
|
||||
|
||||
<div class="block" v-if="data['upload.provider'] === 's3'">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.media.s3.region')"
|
||||
label-position="on-border" expanded>
|
||||
<b-field :label="$t('settings.media.s3.region')" label-position="on-border" expanded>
|
||||
<b-input v-model="data['upload.s3.aws_default_region']" @input="onS3URLChange"
|
||||
name="upload.s3.aws_default_region"
|
||||
:maxlength="200" placeholder="ap-south-1" />
|
||||
name="upload.s3.aws_default_region" :maxlength="200" placeholder="ap-south-1" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field :label="$t('settings.media.s3.key')"
|
||||
label-position="on-border" expanded>
|
||||
<b-input v-model="data['upload.s3.aws_access_key_id']"
|
||||
name="upload.s3.aws_access_key_id" :maxlength="200" />
|
||||
<b-field :label="$t('settings.media.s3.key')" label-position="on-border" expanded>
|
||||
<b-input v-model="data['upload.s3.aws_access_key_id']" name="upload.s3.aws_access_key_id"
|
||||
:maxlength="200" />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.media.s3.secret')"
|
||||
label-position="on-border" expanded
|
||||
<b-field :label="$t('settings.media.s3.secret')" label-position="on-border" expanded
|
||||
message="Enter a value to change.">
|
||||
<b-input v-model="data['upload.s3.aws_secret_access_key']"
|
||||
name="upload.s3.aws_secret_access_key" type="password"
|
||||
:maxlength="200" />
|
||||
<b-input v-model="data['upload.s3.aws_secret_access_key']" name="upload.s3.aws_secret_access_key"
|
||||
type="password" :maxlength="200" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -66,8 +62,7 @@
|
|||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.media.s3.bucketType')" label-position="on-border">
|
||||
<b-select v-model="data['upload.s3.bucket_type']"
|
||||
name="upload.s3.bucket_type" expanded>
|
||||
<b-select v-model="data['upload.s3.bucket_type']" name="upload.s3.bucket_type" expanded>
|
||||
<option value="private">
|
||||
{{ $t('settings.media.s3.bucketTypePrivate') }}
|
||||
</option>
|
||||
|
@ -79,16 +74,14 @@
|
|||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field :label="$t('settings.media.s3.bucket')"
|
||||
label-position="on-border" expanded>
|
||||
<b-input v-model="data['upload.s3.bucket']" @input="onS3URLChange"
|
||||
name="upload.s3.bucket" :maxlength="200" placeholder="" />
|
||||
<b-field :label="$t('settings.media.s3.bucket')" label-position="on-border" expanded>
|
||||
<b-input v-model="data['upload.s3.bucket']" @input="onS3URLChange" name="upload.s3.bucket" :maxlength="200"
|
||||
placeholder="" />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.media.s3.bucketPath')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.media.s3.bucketPath')" label-position="on-border"
|
||||
:message="$t('settings.media.s3.bucketPathHelp')" expanded>
|
||||
<b-input v-model="data['upload.s3.bucket_path']"
|
||||
name="upload.s3.bucket_path" :maxlength="200" placeholder="/" />
|
||||
<b-input v-model="data['upload.s3.bucket_path']" name="upload.s3.bucket_path" :maxlength="200"
|
||||
placeholder="/" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -96,26 +89,22 @@
|
|||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.media.s3.uploadExpiry')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.media.s3.uploadExpiry')" label-position="on-border"
|
||||
:message="$t('settings.media.s3.uploadExpiryHelp')" expanded>
|
||||
<b-input v-model="data['upload.s3.expiry']"
|
||||
name="upload.s3.expiry"
|
||||
placeholder="14d" :pattern="regDuration" :maxlength="10" />
|
||||
<b-input v-model="data['upload.s3.expiry']" name="upload.s3.expiry" placeholder="14d" :pattern="regDuration"
|
||||
:maxlength="10" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<b-field :label="$t('settings.media.s3.url')" label-position="on-border"
|
||||
:message="$t('settings.media.s3.urlHelp')" expanded>
|
||||
<b-input v-model="data['upload.s3.url']"
|
||||
name="upload.s3.url" :disabled="!data['upload.s3.bucket']" required
|
||||
<b-input v-model="data['upload.s3.url']" name="upload.s3.url" :disabled="!data['upload.s3.bucket']" required
|
||||
placeholder="https://s3.$region.amazonaws.com" :maxlength="200" />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.media.s3.publicURL')" label-position="on-border" expanded>
|
||||
<b-input v-model="data['upload.s3.public_url']"
|
||||
:message="$t('settings.media.s3.publicURLHelp')"
|
||||
name="upload.s3.public_url" :disabled="!data['upload.s3.bucket']"
|
||||
placeholder="https://files.yourdomain.com" :maxlength="200" />
|
||||
<b-input v-model="data['upload.s3.public_url']" :message="$t('settings.media.s3.publicURLHelp')"
|
||||
name="upload.s3.public_url" :disabled="!data['upload.s3.bucket']" placeholder="https://files.yourdomain.com"
|
||||
:maxlength="200" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -130,7 +119,7 @@ import { regDuration } from '../../constants';
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -5,32 +5,29 @@
|
|||
<div class="columns">
|
||||
<div class="column is-2">
|
||||
<b-field :label="$t('globals.buttons.enabled')">
|
||||
<b-switch v-model="item.enabled" name="enabled"
|
||||
:native-value="true" />
|
||||
<b-switch v-model="item.enabled" name="enabled" :native-value="true" />
|
||||
</b-field>
|
||||
<b-field>
|
||||
<a @click.prevent="$utils.confirm(null, () => removeMessenger(n))"
|
||||
href="#" class="is-size-7">
|
||||
<a @click.prevent="$utils.confirm(null, () => removeMessenger(n))" href="#" class="is-size-7">
|
||||
<b-icon icon="trash-can-outline" size="is-small" />
|
||||
{{ $t('globals.buttons.delete') }}
|
||||
</a>
|
||||
</b-field>
|
||||
</div><!-- first column -->
|
||||
|
||||
<div class="column" :class="{'disabled': !item.enabled}">
|
||||
<div class="column" :class="{ disabled: !item.enabled }">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('globals.fields.name')" label-position="on-border"
|
||||
:message="$t('settings.messengers.nameHelp')">
|
||||
<b-input v-model="item.name" name="name"
|
||||
placeholder='mymessenger' :maxlength="200" />
|
||||
<b-input v-model="item.name" name="name" placeholder="mymessenger" :maxlength="200" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<b-field :label="$t('settings.messengers.url')" label-position="on-border"
|
||||
:message="$t('settings.messengers.urlHelp')">
|
||||
<b-input v-model="item.root_url" name="root_url"
|
||||
placeholder='https://postback.messenger.net/path' :maxlength="200" />
|
||||
<b-input v-model="item.root_url" name="root_url" placeholder="https://postback.messenger.net/path"
|
||||
:maxlength="200" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div><!-- host -->
|
||||
|
@ -38,17 +35,13 @@
|
|||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field :label="$t('settings.messengers.username')"
|
||||
label-position="on-border" expanded>
|
||||
<b-field :label="$t('settings.messengers.username')" label-position="on-border" expanded>
|
||||
<b-input v-model="item.username" name="username" :maxlength="200" />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.messengers.password')"
|
||||
label-position="on-border" expanded
|
||||
<b-field :label="$t('settings.messengers.password')" label-position="on-border" expanded
|
||||
:message="$t('globals.messages.passwordChange')">
|
||||
<b-input v-model="item.password"
|
||||
name="password" type="password"
|
||||
:placeholder="$t('globals.messages.passwordChange')"
|
||||
:maxlength="200" />
|
||||
<b-input v-model="item.password" name="password" type="password"
|
||||
:placeholder="$t('globals.messages.passwordChange')" :maxlength="200" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -57,30 +50,24 @@
|
|||
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.messengers.maxConns')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.messengers.maxConns')" label-position="on-border"
|
||||
:message="$t('settings.messengers.maxConnsHelp')">
|
||||
<b-numberinput v-model="item.max_conns" name="max_conns" type="is-light"
|
||||
controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
<b-numberinput v-model="item.max_conns" name="max_conns" type="is-light" controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.messengers.retries')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.messengers.retries')" label-position="on-border"
|
||||
:message="$t('settings.messengers.retriesHelp')">
|
||||
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries"
|
||||
type="is-light"
|
||||
controls-position="compact"
|
||||
placeholder="2" min="1" max="1000" />
|
||||
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries" type="is-light"
|
||||
controls-position="compact" placeholder="2" min="1" max="1000" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.messengers.timeout')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.messengers.timeout')" label-position="on-border"
|
||||
:message="$t('settings.messengers.timeoutHelp')">
|
||||
<b-input v-model="item.timeout" name="timeout"
|
||||
placeholder="5s" :pattern="regDuration" :maxlength="10" />
|
||||
<b-input v-model="item.timeout" name="timeout" placeholder="5s" :pattern="regDuration"
|
||||
:maxlength="10" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -103,7 +90,7 @@ import { regDuration } from '../../constants';
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -2,31 +2,26 @@
|
|||
<div class="items">
|
||||
<b-field :label="$t('settings.performance.concurrency')" label-position="on-border"
|
||||
:message="$t('settings.performance.concurrencyHelp')">
|
||||
<b-numberinput v-model="data['app.concurrency']"
|
||||
name="app.concurrency" type="is-light"
|
||||
placeholder="5" min="1" max="10000" />
|
||||
<b-numberinput v-model="data['app.concurrency']" name="app.concurrency" type="is-light" placeholder="5" min="1"
|
||||
max="10000" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.performance.messageRate')" label-position="on-border"
|
||||
:message="$t('settings.performance.messageRateHelp')">
|
||||
<b-numberinput v-model="data['app.message_rate']"
|
||||
name="app.message_rate" type="is-light"
|
||||
placeholder="5" min="1" max="100000" />
|
||||
<b-numberinput v-model="data['app.message_rate']" name="app.message_rate" type="is-light" placeholder="5" min="1"
|
||||
max="100000" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.performance.batchSize')" label-position="on-border"
|
||||
:message="$t('settings.performance.batchSizeHelp')">
|
||||
<b-numberinput v-model="data['app.batch_size']"
|
||||
name="app.batch_size" type="is-light"
|
||||
placeholder="1000" min="1" max="100000" />
|
||||
<b-numberinput v-model="data['app.batch_size']" name="app.batch_size" type="is-light" placeholder="1000" min="1"
|
||||
max="100000" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.performance.maxErrThreshold')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.performance.maxErrThreshold')" label-position="on-border"
|
||||
:message="$t('settings.performance.maxErrThresholdHelp')">
|
||||
<b-numberinput v-model="data['app.max_send_errors']"
|
||||
name="app.max_send_errors" type="is-light"
|
||||
placeholder="1999" min="0" max="100000" />
|
||||
<b-numberinput v-model="data['app.max_send_errors']" name="app.max_send_errors" type="is-light" placeholder="1999"
|
||||
min="0" max="100000" />
|
||||
</b-field>
|
||||
|
||||
<div>
|
||||
|
@ -34,35 +29,24 @@
|
|||
<div class="column is-6">
|
||||
<b-field :label="$t('settings.performance.slidingWindow')"
|
||||
:message="$t('settings.performance.slidingWindowHelp')">
|
||||
<b-switch v-model="data['app.message_sliding_window']"
|
||||
name="app.message_sliding_window" />
|
||||
<b-switch v-model="data['app.message_sliding_window']" name="app.message_sliding_window" />
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div class="column is-3"
|
||||
:class="{'disabled': !data['app.message_sliding_window']}">
|
||||
<b-field :label="$t('settings.performance.slidingWindowRate')"
|
||||
label-position="on-border"
|
||||
<div class="column is-3" :class="{ disabled: !data['app.message_sliding_window'] }">
|
||||
<b-field :label="$t('settings.performance.slidingWindowRate')" label-position="on-border"
|
||||
:message="$t('settings.performance.slidingWindowRateHelp')">
|
||||
|
||||
<b-numberinput v-model="data['app.message_sliding_window_rate']"
|
||||
name="sliding_window_rate" type="is-light"
|
||||
controls-position="compact"
|
||||
:disabled="!data['app.message_sliding_window']"
|
||||
placeholder="25" min="1" max="10000000" />
|
||||
<b-numberinput v-model="data['app.message_sliding_window_rate']" name="sliding_window_rate" type="is-light"
|
||||
controls-position="compact" :disabled="!data['app.message_sliding_window']" placeholder="25" min="1"
|
||||
max="10000000" />
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div class="column is-3"
|
||||
:class="{'disabled': !data['app.message_sliding_window']}">
|
||||
<b-field :label="$t('settings.performance.slidingWindowDuration')"
|
||||
label-position="on-border"
|
||||
<div class="column is-3" :class="{ disabled: !data['app.message_sliding_window'] }">
|
||||
<b-field :label="$t('settings.performance.slidingWindowDuration')" label-position="on-border"
|
||||
:message="$t('settings.performance.slidingWindowDurationHelp')">
|
||||
|
||||
<b-input v-model="data['app.message_sliding_window_duration']"
|
||||
name="sliding_window_duration"
|
||||
:disabled="!data['app.message_sliding_window']"
|
||||
placeholder="1h" :pattern="regDuration" :maxlength="10" />
|
||||
<b-input v-model="data['app.message_sliding_window_duration']" name="sliding_window_duration"
|
||||
:disabled="!data['app.message_sliding_window']" placeholder="1h" :pattern="regDuration" :maxlength="10" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -77,7 +61,7 @@ import { regDuration } from '../../constants';
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -2,51 +2,35 @@
|
|||
<div class="items">
|
||||
<b-field :label="$t('settings.privacy.individualSubTracking')"
|
||||
:message="$t('settings.privacy.individualSubTrackingHelp')">
|
||||
<b-switch v-model="data['privacy.individual_tracking']"
|
||||
name="privacy.individual_tracking" />
|
||||
<b-switch v-model="data['privacy.individual_tracking']" name="privacy.individual_tracking" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.privacy.listUnsubHeader')"
|
||||
:message="$t('settings.privacy.listUnsubHeaderHelp')">
|
||||
<b-switch v-model="data['privacy.unsubscribe_header']"
|
||||
name="privacy.unsubscribe_header" />
|
||||
<b-field :label="$t('settings.privacy.listUnsubHeader')" :message="$t('settings.privacy.listUnsubHeaderHelp')">
|
||||
<b-switch v-model="data['privacy.unsubscribe_header']" name="privacy.unsubscribe_header" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.privacy.allowBlocklist')"
|
||||
:message="$t('settings.privacy.allowBlocklistHelp')">
|
||||
<b-switch v-model="data['privacy.allow_blocklist']"
|
||||
name="privacy.allow_blocklist" />
|
||||
<b-field :label="$t('settings.privacy.allowBlocklist')" :message="$t('settings.privacy.allowBlocklistHelp')">
|
||||
<b-switch v-model="data['privacy.allow_blocklist']" name="privacy.allow_blocklist" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.privacy.allowPrefs')"
|
||||
:message="$t('settings.privacy.allowPrefsHelp')">
|
||||
<b-switch v-model="data['privacy.allow_preferences']"
|
||||
name="privacy.allow_blocklist" />
|
||||
<b-field :label="$t('settings.privacy.allowPrefs')" :message="$t('settings.privacy.allowPrefsHelp')">
|
||||
<b-switch v-model="data['privacy.allow_preferences']" name="privacy.allow_blocklist" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.privacy.allowExport')"
|
||||
:message="$t('settings.privacy.allowExportHelp')">
|
||||
<b-switch v-model="data['privacy.allow_export']"
|
||||
name="privacy.allow_export" />
|
||||
<b-field :label="$t('settings.privacy.allowExport')" :message="$t('settings.privacy.allowExportHelp')">
|
||||
<b-switch v-model="data['privacy.allow_export']" name="privacy.allow_export" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.privacy.allowWipe')"
|
||||
:message="$t('settings.privacy.allowWipeHelp')">
|
||||
<b-switch v-model="data['privacy.allow_wipe']"
|
||||
name="privacy.allow_wipe" />
|
||||
<b-field :label="$t('settings.privacy.allowWipe')" :message="$t('settings.privacy.allowWipeHelp')">
|
||||
<b-switch v-model="data['privacy.allow_wipe']" name="privacy.allow_wipe" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.privacy.recordOptinIP')"
|
||||
:message="$t('settings.privacy.recordOptinIPHelp')">
|
||||
<b-switch v-model="data['privacy.record_optin_ip']"
|
||||
name="privacy.record_optin_ip" />
|
||||
<b-field :label="$t('settings.privacy.recordOptinIP')" :message="$t('settings.privacy.recordOptinIPHelp')">
|
||||
<b-switch v-model="data['privacy.record_optin_ip']" name="privacy.record_optin_ip" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('settings.privacy.domainBlocklist')"
|
||||
:message="$t('settings.privacy.domainBlocklistHelp')">
|
||||
<b-input type="textarea"
|
||||
v-model="data['privacy.domain_blocklist']"
|
||||
name="privacy.domain_blocklist" />
|
||||
<b-field :label="$t('settings.privacy.domainBlocklist')" :message="$t('settings.privacy.domainBlocklistHelp')">
|
||||
<b-input type="textarea" v-model="data['privacy.domain_blocklist']" name="privacy.domain_blocklist" />
|
||||
</b-field>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -57,7 +41,7 @@ import Vue from 'vue';
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -2,21 +2,19 @@
|
|||
<div class="items">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.security.enableCaptcha')"
|
||||
:message="$t('settings.security.enableCaptchaHelp')">
|
||||
<b-switch v-model="data['security.enable_captcha']"
|
||||
name="security.captcha" />
|
||||
<b-field :label="$t('settings.security.enableCaptcha')" :message="$t('settings.security.enableCaptchaHelp')">
|
||||
<b-switch v-model="data['security.enable_captcha']" name="security.captcha" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<b-field :label="$t('settings.security.captchaKey')" label-position="on-border"
|
||||
:message="$t('settings.security.captchaKeyHelp')">
|
||||
<b-input v-model="data['security.captcha_key']" name="captcha_key"
|
||||
:disabled="!data['security.enable_captcha']" :maxlength="200" required />
|
||||
<b-input v-model="data['security.captcha_key']" name="captcha_key" :disabled="!data['security.enable_captcha']"
|
||||
:maxlength="200" required />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.security.captchaSecret')" label-position="on-border">
|
||||
<b-input v-model="data['security.captcha_secret']" name="captcha_secret" type="password"
|
||||
:disabled="!data['security.enable_captcha']" :maxlength="200" required />
|
||||
:disabled="!data['security.enable_captcha']" :maxlength="200" required />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,7 +27,7 @@ import Vue from 'vue';
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -5,77 +5,74 @@
|
|||
<div class="columns">
|
||||
<div class="column is-2">
|
||||
<b-field :label="$t('globals.buttons.enabled')">
|
||||
<b-switch v-model="item.enabled" name="enabled"
|
||||
:native-value="true" data-cy="btn-enable-smtp" />
|
||||
<b-switch v-model="item.enabled" name="enabled" :native-value="true" data-cy="btn-enable-smtp" />
|
||||
</b-field>
|
||||
<b-field v-if="form.smtp.length > 1">
|
||||
<a @click.prevent="$utils.confirm(null, () => removeSMTP(n))"
|
||||
href="#" data-cy="btn-delete-smtp">
|
||||
<a @click.prevent="$utils.confirm(null, () => removeSMTP(n))" href="#" data-cy="btn-delete-smtp">
|
||||
<b-icon icon="trash-can-outline" />
|
||||
{{ $t('globals.buttons.delete') }}
|
||||
</a>
|
||||
</b-field>
|
||||
</div><!-- first column -->
|
||||
|
||||
<div class="column" :class="{'disabled': !item.enabled}">
|
||||
<div class="column" :class="{ disabled: !item.enabled }">
|
||||
<div class="columns">
|
||||
<div class="column is-8">
|
||||
<b-field :label="$t('settings.mailserver.host')" label-position="on-border"
|
||||
:message="$t('settings.mailserver.hostHelp')">
|
||||
<b-input v-model="item.host" name="host"
|
||||
placeholder='smtp.yourmailserver.net' :maxlength="200" />
|
||||
<b-input v-model="item.host" name="host" placeholder="smtp.yourmailserver.net" :maxlength="200" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('settings.mailserver.port')" label-position="on-border"
|
||||
:message="$t('settings.mailserver.portHelp')">
|
||||
<b-numberinput v-model="item.port" name="port" type="is-light"
|
||||
controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
<b-numberinput v-model="item.port" name="port" type="is-light" controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div><!-- host -->
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-2">
|
||||
<b-field :label="$t('settings.mailserver.authProtocol')"
|
||||
label-position="on-border">
|
||||
<b-field :label="$t('settings.mailserver.authProtocol')" label-position="on-border">
|
||||
<b-select v-model="item.auth_protocol" name="auth_protocol">
|
||||
<option value="login">LOGIN</option>
|
||||
<option value="cram">CRAM</option>
|
||||
<option value="plain">PLAIN</option>
|
||||
<option value="none">None</option>
|
||||
<option value="login">
|
||||
LOGIN
|
||||
</option>
|
||||
<option value="cram">
|
||||
CRAM
|
||||
</option>
|
||||
<option value="plain">
|
||||
PLAIN
|
||||
</option>
|
||||
<option value="none">
|
||||
None
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field :label="$t('settings.mailserver.username')"
|
||||
label-position="on-border" expanded>
|
||||
<b-field :label="$t('settings.mailserver.username')" label-position="on-border" expanded>
|
||||
<b-input v-model="item.username" :custom-class="`smtp-username-${n}`"
|
||||
:disabled="item.auth_protocol === 'none'"
|
||||
name="username" placeholder="mysmtp" :maxlength="200" />
|
||||
:disabled="item.auth_protocol === 'none'" name="username" placeholder="mysmtp" :maxlength="200" />
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.mailserver.password')"
|
||||
label-position="on-border" expanded
|
||||
<b-field :label="$t('settings.mailserver.password')" label-position="on-border" expanded
|
||||
:message="$t('settings.mailserver.passwordHelp')">
|
||||
<b-input v-model="item.password"
|
||||
:disabled="item.auth_protocol === 'none'"
|
||||
name="password" type="password"
|
||||
:custom-class="`password-${n}`"
|
||||
:placeholder="$t('settings.mailserver.passwordHelp')"
|
||||
:maxlength="200" />
|
||||
<b-input v-model="item.password" :disabled="item.auth_protocol === 'none'" name="password"
|
||||
type="password" :custom-class="`password-${n}`"
|
||||
:placeholder="$t('settings.mailserver.passwordHelp')" :maxlength="200" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
</div><!-- auth -->
|
||||
<div class="smtp-shortcuts is-size-7">
|
||||
<a href="" @click.prevent="() => fillSettings(n, 'gmail')">Gmail</a>
|
||||
<a href="" @click.prevent="() => fillSettings(n, 'ses')">Amazon SES</a>
|
||||
<a href="" @click.prevent="() => fillSettings(n, 'mailgun')">Mailgun</a>
|
||||
<a href="" @click.prevent="() => fillSettings(n, 'mailjet')">Mailjet</a>
|
||||
<a href="" @click.prevent="() => fillSettings(n, 'sendgrid')">Sendgrid</a>
|
||||
<a href="" @click.prevent="() => fillSettings(n, 'postmark')">Postmark</a>
|
||||
<a href="#" @click.prevent="() => fillSettings(n, 'gmail')">Gmail</a>
|
||||
<a href="#" @click.prevent="() => fillSettings(n, 'ses')">Amazon SES</a>
|
||||
<a href="#" @click.prevent="() => fillSettings(n, 'mailgun')">Mailgun</a>
|
||||
<a href="#" @click.prevent="() => fillSettings(n, 'mailjet')">Mailjet</a>
|
||||
<a href="#" @click.prevent="() => fillSettings(n, 'sendgrid')">Sendgrid</a>
|
||||
<a href="#" @click.prevent="() => fillSettings(n, 'postmark')">Postmark</a>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
|
@ -83,24 +80,29 @@
|
|||
<div class="column is-6">
|
||||
<b-field :label="$t('settings.smtp.heloHost')" label-position="on-border"
|
||||
:message="$t('settings.smtp.heloHostHelp')">
|
||||
<b-input v-model="item.hello_hostname"
|
||||
name="hello_hostname" placeholder="" :maxlength="200" />
|
||||
<b-input v-model="item.hello_hostname" name="hello_hostname" placeholder="" :maxlength="200" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field grouped>
|
||||
<b-field :label="$t('settings.mailserver.tls')" expanded
|
||||
:message="$t('settings.mailserver.tlsHelp')" label-position="on-border">
|
||||
<b-field :label="$t('settings.mailserver.tls')" expanded :message="$t('settings.mailserver.tlsHelp')"
|
||||
label-position="on-border">
|
||||
<b-select v-model="item.tls_type" name="items.tls_type">
|
||||
<option value="none">{{ $t('globals.states.off') }}</option>
|
||||
<option value="STARTTLS">STARTTLS</option>
|
||||
<option value="TLS">SSL/TLS</option>
|
||||
<option value="none">
|
||||
{{ $t('globals.states.off') }}
|
||||
</option>
|
||||
<option value="STARTTLS">
|
||||
STARTTLS
|
||||
</option>
|
||||
<option value="TLS">
|
||||
SSL/TLS
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-field :label="$t('settings.mailserver.skipTLS')" expanded
|
||||
:message="$t('settings.mailserver.skipTLSHelp')">
|
||||
<b-switch v-model="item.tls_skip_verify"
|
||||
:disabled="item.tls_type === 'none'" name="item.tls_skip_verify" />
|
||||
<b-switch v-model="item.tls_skip_verify" :disabled="item.tls_type === 'none'"
|
||||
name="item.tls_skip_verify" />
|
||||
</b-field>
|
||||
</b-field>
|
||||
</div>
|
||||
|
@ -109,37 +111,31 @@
|
|||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.mailserver.maxConns')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.mailserver.maxConns')" label-position="on-border"
|
||||
:message="$t('settings.mailserver.maxConnsHelp')">
|
||||
<b-numberinput v-model="item.max_conns" name="max_conns" type="is-light"
|
||||
controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
<b-numberinput v-model="item.max_conns" name="max_conns" type="is-light" controls-position="compact"
|
||||
placeholder="25" min="1" max="65535" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.smtp.retries')" label-position="on-border"
|
||||
:message="$t('settings.smtp.retriesHelp')">
|
||||
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries"
|
||||
type="is-light"
|
||||
controls-position="compact"
|
||||
placeholder="2" min="1" max="1000" />
|
||||
<b-numberinput v-model="item.max_msg_retries" name="max_msg_retries" type="is-light"
|
||||
controls-position="compact" placeholder="2" min="1" max="1000" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.mailserver.idleTimeout')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.mailserver.idleTimeout')" label-position="on-border"
|
||||
:message="$t('settings.mailserver.idleTimeoutHelp')">
|
||||
<b-input v-model="item.idle_timeout" name="idle_timeout"
|
||||
placeholder="15s" :pattern="regDuration" :maxlength="10" />
|
||||
<b-input v-model="item.idle_timeout" name="idle_timeout" placeholder="15s" :pattern="regDuration"
|
||||
:maxlength="10" />
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<b-field :label="$t('settings.mailserver.waitTimeout')"
|
||||
label-position="on-border"
|
||||
<b-field :label="$t('settings.mailserver.waitTimeout')" label-position="on-border"
|
||||
:message="$t('settings.mailserver.waitTimeoutHelp')">
|
||||
<b-input v-model="item.wait_timeout" name="wait_timeout"
|
||||
placeholder="5s" :pattern="regDuration" :maxlength="10" />
|
||||
<b-input v-model="item.wait_timeout" name="wait_timeout" placeholder="5s" :pattern="regDuration"
|
||||
:maxlength="10" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -150,11 +146,10 @@
|
|||
<a href="#" @click.prevent="() => showSMTPHeaders(n)">
|
||||
<b-icon icon="plus" />{{ $t('settings.smtp.setCustomHeaders') }}</a>
|
||||
</p>
|
||||
<b-field v-if="item.email_headers.length > 0 || item.showHeaders"
|
||||
label-position="on-border"
|
||||
<b-field v-if="item.email_headers.length > 0 || item.showHeaders" label-position="on-border"
|
||||
:message="$t('settings.smtp.customHeadersHelp')">
|
||||
<b-input v-model="item.strEmailHeaders" name="email_headers" type="textarea"
|
||||
placeholder='[{"X-Custom": "value"}, {"X-Custom2": "value"}]' />
|
||||
placeholder="[{"X-Custom": "value"}, {"X-Custom2": "value"}]" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -170,15 +165,13 @@
|
|||
</div>
|
||||
<div class="column is-4">
|
||||
<b-field :label="$t('settings.smtp.toEmail')" label-position="on-border">
|
||||
<b-input type="email" required v-model="testEmail"
|
||||
:ref="'testEmailTo'" placeholder="email@site.com"
|
||||
<b-input type="email" required v-model="testEmail" :ref="'testEmailTo'" placeholder="email@site.com"
|
||||
:custom-class="`test-email-${n}`" />
|
||||
</b-field>
|
||||
</div>
|
||||
</template>
|
||||
<div class="column has-text-right">
|
||||
<b-button v-if="smtpTestItem === n" class="is-primary"
|
||||
@click.prevent="() => doSMTPTest(item, n)">
|
||||
<b-button v-if="smtpTestItem === n" class="is-primary" @click.prevent="() => doSMTPTest(item, n)">
|
||||
{{ $t('settings.smtp.sendTest') }}
|
||||
</b-button>
|
||||
<a href="#" v-else class="is-primary" @click.prevent="showTestForm(n)">
|
||||
|
@ -186,18 +179,15 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
</div>
|
||||
<div class="column" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errMsg && smtpTestItem === n">
|
||||
<b-field class="mt-4" type="is-danger">
|
||||
<b-input v-model="errMsg" type="textarea"
|
||||
custom-class="has-text-danger is-size-6" readonly />
|
||||
<b-input v-model="errMsg" type="textarea" custom-class="has-text-danger is-size-6" readonly />
|
||||
</b-field>
|
||||
</div>
|
||||
</form><!-- smtp test -->
|
||||
|
||||
</div>
|
||||
</div><!-- second container column -->
|
||||
</div><!-- block -->
|
||||
|
@ -238,7 +228,7 @@ const smtpTemplates = {
|
|||
export default Vue.extend({
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
type: Object, default: () => { },
|
||||
},
|
||||
},
|
||||
|
||||
|
|
37
frontend/vite.config.js
vendored
Normal file
37
frontend/vite.config.js
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
import vue from '@vitejs/plugin-vue2';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ _, mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
return {
|
||||
plugins: [vue()],
|
||||
base: '/admin',
|
||||
mode,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
bulma: require.resolve('bulma/bulma.sass'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
assetsDir: 'static',
|
||||
},
|
||||
server: {
|
||||
port: env.LISTMONK_FRONTEND_PORT || 8080,
|
||||
proxy: {
|
||||
'^/$': {
|
||||
target: env.LISTMONK_API_URL || 'http://127.0.0.1:9000',
|
||||
},
|
||||
'^/(api|webhooks|subscription|public|health)': {
|
||||
target: env.LISTMONK_API_URL || 'http://127.0.0.1:9000',
|
||||
},
|
||||
'^/(admin\/custom\.(css|js))': {
|
||||
target: env.LISTMONK_API_URL || 'http://127.0.0.1:9000',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
40
frontend/vue.config.js
vendored
40
frontend/vue.config.js
vendored
|
@ -1,40 +0,0 @@
|
|||
module.exports = {
|
||||
publicPath: '/admin',
|
||||
outputDir: 'dist',
|
||||
|
||||
// This is to make all static file requests generated by Vue to go to
|
||||
// /frontend/*. However, this also ends up creating a `dist/frontend`
|
||||
// directory and moves all the static files in it. The physical directory
|
||||
// and the URI for assets are tightly coupled. This is handled in the Go app
|
||||
// by using stuffbin aliases.
|
||||
assetsDir: 'static',
|
||||
|
||||
// Move the index.html file from dist/index.html to dist/frontend/index.html
|
||||
// indexPath: './frontend/index.html',
|
||||
|
||||
productionSourceMap: false,
|
||||
filenameHashing: true,
|
||||
|
||||
css: {
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
implementation: require('sass'), // This line must in sass option
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
devServer: {
|
||||
port: process.env.LISTMONK_FRONTEND_PORT || 8080,
|
||||
proxy: {
|
||||
'^/$': {
|
||||
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
||||
},
|
||||
'^/(api|webhooks|subscription|public|health)': {
|
||||
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
||||
},
|
||||
'^/(admin\/custom\.(css|js))': {
|
||||
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
6432
frontend/yarn.lock
vendored
6432
frontend/yarn.lock
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue