Refactor fetching of server config and settings.
The earlier approach of loading `/api/config.js` as a script on initial page load with the necessary variables to init the UI is ditched. Instead, it's now `/api/config` and `/api/settings` like all other API calls. On load of the frontend, these two resources are fetched and the frontend is initialised.
This commit is contained in:
parent
b6dcf2c841
commit
b950d2f4ff
48
cmd/admin.go
48
cmd/admin.go
|
@ -1,8 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -13,41 +11,29 @@ import (
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type configScript struct {
|
type serverConfig struct {
|
||||||
RootURL string `json:"rootURL"`
|
Messengers []string `json:"messengers"`
|
||||||
FromEmail string `json:"fromEmail"`
|
Langs []i18nLang `json:"langs"`
|
||||||
Messengers []string `json:"messengers"`
|
Lang string `json:"lang"`
|
||||||
MediaProvider string `json:"mediaProvider"`
|
Update *AppUpdate `json:"update"`
|
||||||
NeedsRestart bool `json:"needsRestart"`
|
NeedsRestart bool `json:"needs_restart"`
|
||||||
Update *AppUpdate `json:"update"`
|
|
||||||
Langs []i18nLang `json:"langs"`
|
|
||||||
EnablePublicSubPage bool `json:"enablePublicSubscriptionPage"`
|
|
||||||
Lang json.RawMessage `json:"lang"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetConfigScript returns general configuration as a Javascript
|
// handleGetServerConfig returns general server config.
|
||||||
// variable that can be included in an HTML page directly.
|
func handleGetServerConfig(c echo.Context) error {
|
||||||
func handleGetConfigScript(c echo.Context) error {
|
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
out = configScript{
|
out = serverConfig{}
|
||||||
RootURL: app.constants.RootURL,
|
|
||||||
FromEmail: app.constants.FromEmail,
|
|
||||||
MediaProvider: app.constants.MediaProvider,
|
|
||||||
EnablePublicSubPage: app.constants.EnablePublicSubPage,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Language list.
|
// Language list.
|
||||||
langList, err := geti18nLangList(app.constants.Lang, app)
|
langList, err := getI18nLangList(app.constants.Lang, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error loading language list: %v", err))
|
fmt.Sprintf("Error loading language list: %v", err))
|
||||||
}
|
}
|
||||||
out.Langs = langList
|
out.Langs = langList
|
||||||
|
out.Lang = app.constants.Lang
|
||||||
// Current language.
|
|
||||||
out.Lang = json.RawMessage(app.i18n.JSON())
|
|
||||||
|
|
||||||
// Sort messenger names with `email` always as the first item.
|
// Sort messenger names with `email` always as the first item.
|
||||||
var names []string
|
var names []string
|
||||||
|
@ -66,17 +52,7 @@ func handleGetConfigScript(c echo.Context) error {
|
||||||
out.Update = app.update
|
out.Update = app.update
|
||||||
app.Unlock()
|
app.Unlock()
|
||||||
|
|
||||||
// Write the Javascript variable opening;
|
return c.JSON(http.StatusOK, okResp{out})
|
||||||
b := bytes.Buffer{}
|
|
||||||
b.Write([]byte(`var CONFIG = `))
|
|
||||||
|
|
||||||
// Encode the config payload as JSON and write as the variable's value assignment.
|
|
||||||
j := json.NewEncoder(&b)
|
|
||||||
if err := j.Encode(out); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
|
||||||
app.i18n.Ts("admin.errorMarshallingConfig", "error", err.Error()))
|
|
||||||
}
|
|
||||||
return c.Blob(http.StatusOK, "application/javascript; charset=utf-8", b.Bytes())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetDashboardCharts returns chart data points to render ont he dashboard.
|
// handleGetDashboardCharts returns chart data points to render ont he dashboard.
|
||||||
|
|
|
@ -2,8 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -44,8 +42,8 @@ func registerHTTPHandlers(e *echo.Echo) {
|
||||||
g := e.Group("", middleware.BasicAuth(basicAuth))
|
g := e.Group("", middleware.BasicAuth(basicAuth))
|
||||||
g.GET("/", handleIndexPage)
|
g.GET("/", handleIndexPage)
|
||||||
g.GET("/api/health", handleHealthCheck)
|
g.GET("/api/health", handleHealthCheck)
|
||||||
g.GET("/api/config.js", handleGetConfigScript)
|
g.GET("/api/config", handleGetServerConfig)
|
||||||
g.GET("/api/lang/:lang", handleLoadLanguage)
|
g.GET("/api/lang/:lang", handleGetI18nLang)
|
||||||
g.GET("/api/dashboard/charts", handleGetDashboardCharts)
|
g.GET("/api/dashboard/charts", handleGetDashboardCharts)
|
||||||
g.GET("/api/dashboard/counts", handleGetDashboardCounts)
|
g.GET("/api/dashboard/counts", handleGetDashboardCounts)
|
||||||
|
|
||||||
|
@ -164,23 +162,6 @@ func handleHealthCheck(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, okResp{true})
|
return c.JSON(http.StatusOK, okResp{true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLoadLanguage returns the JSON language pack given the language code.
|
|
||||||
func handleLoadLanguage(c echo.Context) error {
|
|
||||||
app := c.Get("app").(*App)
|
|
||||||
|
|
||||||
lang := c.Param("lang")
|
|
||||||
if len(lang) > 6 || reLangCode.MatchString(lang) {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.")
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := app.fs.Read(fmt.Sprintf("/lang/%s.json", lang))
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, okResp{json.RawMessage(b)})
|
|
||||||
}
|
|
||||||
|
|
||||||
// basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
|
// basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
|
||||||
func basicAuth(username, password string, c echo.Context) (bool, error) {
|
func basicAuth(username, password string, c echo.Context) (bool, error) {
|
||||||
app := c.Get("app").(*App)
|
app := c.Get("app").(*App)
|
||||||
|
|
53
cmd/i18n.go
53
cmd/i18n.go
|
@ -3,6 +3,11 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/knadh/listmonk/internal/i18n"
|
||||||
|
"github.com/knadh/stuffbin"
|
||||||
|
"github.com/labstack/echo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type i18nLang struct {
|
type i18nLang struct {
|
||||||
|
@ -15,8 +20,25 @@ type i18nLangRaw struct {
|
||||||
Name string `json:"_.name"`
|
Name string `json:"_.name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// geti18nLangList returns the list of available i18n languages.
|
// handleGetI18nLang returns the JSON language pack given the language code.
|
||||||
func geti18nLangList(lang string, app *App) ([]i18nLang, error) {
|
func handleGetI18nLang(c echo.Context) error {
|
||||||
|
app := c.Get("app").(*App)
|
||||||
|
|
||||||
|
lang := c.Param("lang")
|
||||||
|
if len(lang) > 6 || reLangCode.MatchString(lang) {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.")
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := getI18nLang(lang, app.fs)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, okResp{json.RawMessage(i.JSON())})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getI18nLangList returns the list of available i18n languages.
|
||||||
|
func getI18nLangList(lang string, app *App) ([]i18nLang, error) {
|
||||||
list, err := app.fs.Glob("/i18n/*.json")
|
list, err := app.fs.Glob("/i18n/*.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -42,3 +64,30 @@ func geti18nLangList(lang string, app *App) ([]i18nLang, error) {
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getI18nLang(lang string, fs stuffbin.FileSystem) (*i18n.I18n, error) {
|
||||||
|
const def = "en"
|
||||||
|
|
||||||
|
b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading default i18n language file: %s: %v", def, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with the default language.
|
||||||
|
i, err := i18n.New(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshalling i18n language: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the selected language on top of it.
|
||||||
|
b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading i18n language file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Load(b); err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading i18n language file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
22
cmd/init.go
22
cmd/init.go
|
@ -262,28 +262,10 @@ func initConstants() *constants {
|
||||||
// and then the selected language is loaded on top of it so that if there are
|
// and then the selected language is loaded on top of it so that if there are
|
||||||
// missing translations in it, the default English translations show up.
|
// missing translations in it, the default English translations show up.
|
||||||
func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
|
func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
|
||||||
const def = "en"
|
i, err := getI18nLang(lang, fs)
|
||||||
|
|
||||||
b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lo.Fatalf("error reading default i18n language file: %s: %v", def, err)
|
lo.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with the default language.
|
|
||||||
i, err := i18n.New(b)
|
|
||||||
if err != nil {
|
|
||||||
lo.Fatalf("error unmarshalling i18n language: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the selected language on top of it.
|
|
||||||
b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang))
|
|
||||||
if err != nil {
|
|
||||||
lo.Fatalf("error reading i18n language file: %v", err)
|
|
||||||
}
|
|
||||||
if err := i.Load(b); err != nil {
|
|
||||||
lo.Fatalf("error loading i18n language file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,10 @@ It's best if the `listmonk/frontend` directory is opened in an IDE as a separate
|
||||||
For developer setup instructions, refer to the main project's README.
|
For developer setup instructions, refer to the main project's README.
|
||||||
|
|
||||||
## Globals
|
## Globals
|
||||||
`main.js` is where Buefy is injected globally into Vue. In addition two controllers, `$api` (collection of API calls from `api/index.js`), `$utils` (util functions from `util.js`), `$serverConfig` (loaded form /api/config.js) are also attached globaly to Vue. They are accessible within Vue as `this.$api` and `this.$utils`.
|
In `main.js`, Buefy and vue-i18n are attached globally. In addition:
|
||||||
|
|
||||||
|
- `$api` (collection of API calls from `api/index.js`)
|
||||||
|
- `$utils` (util functions from `util.js`). They are accessible within Vue as `this.$api` and `this.$utils`.
|
||||||
|
|
||||||
Some constants are defined in `constants.js`.
|
Some constants are defined in `constants.js`.
|
||||||
|
|
||||||
|
@ -14,7 +17,7 @@ The project uses a global `vuex` state to centrally store the responses to prett
|
||||||
|
|
||||||
There is a global state `loading` (eg: loading.campaigns, loading.lists) that indicates whether an API call for that particular "model" is running. This can be used anywhere in the project to show loading spinners for instance. All the API definitions are in `api/index.js`. It also describes how each API call sets the global `loading` status alongside storing the API responses.
|
There is a global state `loading` (eg: loading.campaigns, loading.lists) that indicates whether an API call for that particular "model" is running. This can be used anywhere in the project to show loading spinners for instance. All the API definitions are in `api/index.js`. It also describes how each API call sets the global `loading` status alongside storing the API responses.
|
||||||
|
|
||||||
*IMPORTANT*: All JSON field names in GET API responses are automatically camel-cased when they're pulled for the sake of consistentcy in the frontend code and for complying with the linter spec in the project (Vue/AirBnB schema). For example, `content_type` becomes `contentType`. When sending responses to the backend, however, they should be snake-cased manually.
|
*IMPORTANT*: All JSON field names in GET API responses are automatically camel-cased when they're pulled for the sake of consistentcy in the frontend code and for complying with the linter spec in the project (Vue/AirBnB schema). For example, `content_type` becomes `contentType`. When sending responses to the backend, however, they should be snake-cased manually. This is overridden for certain calls such as `/api/config` and `/api/settings` using the `preserveCase: true` param in `api/index.js`.
|
||||||
|
|
||||||
|
|
||||||
## Icon pack
|
## Icon pack
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
|
<link rel="icon" href="<%= BASE_URL %>frontend/favicon.png" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" />
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
<script src="<%= BASE_URL %>api/config.js" id="server-config"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<b-navbar :fixed-top="true">
|
<b-navbar :fixed-top="true" v-if="$root.isLoaded">
|
||||||
<template slot="brand">
|
<template slot="brand">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<router-link :to="{name: 'dashboard'}">
|
<router-link :to="{name: 'dashboard'}">
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
</template>
|
</template>
|
||||||
</b-navbar>
|
</b-navbar>
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper" v-if="$root.isLoaded">
|
||||||
<section class="sidebar">
|
<section class="sidebar">
|
||||||
<b-sidebar
|
<b-sidebar
|
||||||
position="static"
|
position="static"
|
||||||
|
@ -100,18 +100,17 @@
|
||||||
|
|
||||||
<!-- body //-->
|
<!-- body //-->
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="global-notices" v-if="serverConfig.needsRestart || serverConfig.update">
|
<div class="global-notices" v-if="serverConfig.needs_restart || serverConfig.update">
|
||||||
<div v-if="serverConfig.needsRestart" class="notification is-danger">
|
<div v-if="serverConfig.needs_restart" class="notification is-danger">
|
||||||
Settings have changed. Pause all running campaigns and restart the app
|
{{ $t('settings.needsRestart') }}
|
||||||
—
|
—
|
||||||
<b-button class="is-primary" size="is-small"
|
<b-button class="is-primary" size="is-small"
|
||||||
@click="$utils.confirm(
|
@click="$utils.confirm($t('settings.confirmRestart'), reloadApp)">
|
||||||
'Ensure running campaigns are paused. Restart?', reloadApp)">
|
{{ $t('settings.restart') }}
|
||||||
Restart
|
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="serverConfig.update" class="notification is-success">
|
<div v-if="serverConfig.update" class="notification is-success">
|
||||||
A new update ({{ serverConfig.update.version }}) is available.
|
{{ $t('settings.updateAvailable', { version: serverConfig.update.version }) }}
|
||||||
<a :href="serverConfig.update.url" target="_blank">View</a>
|
<a :href="serverConfig.update.url" target="_blank">View</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,15 +119,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-loading v-if="!isLoaded" active>
|
<b-loading v-if="!$root.isLoaded" active />
|
||||||
<div class="has-text-centered">
|
|
||||||
<h1 class="title">Oops</h1>
|
|
||||||
<p>
|
|
||||||
Can't connect to the backend.<br />
|
|
||||||
Make sure the server is running and refresh this page.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</b-loading>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -143,7 +134,6 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
activeItem: {},
|
activeItem: {},
|
||||||
activeGroup: {},
|
activeGroup: {},
|
||||||
isLoaded: window.CONFIG,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -195,11 +195,17 @@ export const deleteTemplate = async (id) => http.delete(`/api/templates/${id}`,
|
||||||
{ loading: models.templates });
|
{ loading: models.templates });
|
||||||
|
|
||||||
// Settings.
|
// Settings.
|
||||||
|
export const getServerConfig = async () => http.get('/api/config',
|
||||||
|
{ loading: models.serverConfig, store: models.serverConfig, preserveCase: true });
|
||||||
|
|
||||||
export const getSettings = async () => http.get('/api/settings',
|
export const getSettings = async () => http.get('/api/settings',
|
||||||
{ loading: models.settings, preserveCase: true });
|
{ loading: models.settings, store: models.settings, preserveCase: true });
|
||||||
|
|
||||||
export const updateSettings = async (data) => http.put('/api/settings', data,
|
export const updateSettings = async (data) => http.put('/api/settings', data,
|
||||||
{ loading: models.settings });
|
{ loading: models.settings });
|
||||||
|
|
||||||
export const getLogs = async () => http.get('/api/logs',
|
export const getLogs = async () => http.get('/api/logs',
|
||||||
{ loading: models.logs });
|
{ loading: models.logs });
|
||||||
|
|
||||||
|
export const getLang = async (lang) => http.get(`/api/lang/${lang}`,
|
||||||
|
{ loading: models.lang, preserveCase: true });
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
export const models = Object.freeze({
|
export const models = Object.freeze({
|
||||||
// This is the config loaded from /api/config.js directly onto the page
|
|
||||||
// via a <script> tag.
|
|
||||||
serverConfig: 'serverConfig',
|
serverConfig: 'serverConfig',
|
||||||
|
lang: 'lang',
|
||||||
dashboard: 'dashboard',
|
dashboard: 'dashboard',
|
||||||
lists: 'lists',
|
lists: 'lists',
|
||||||
subscribers: 'subscribers',
|
subscribers: 'subscribers',
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Buefy from 'buefy';
|
import Buefy from 'buefy';
|
||||||
import humps from 'humps';
|
|
||||||
import VueI18n from 'vue-i18n';
|
import VueI18n from 'vue-i18n';
|
||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import * as api from './api';
|
import * as api from './api';
|
||||||
import { models } from './constants';
|
|
||||||
import Utils from './utils';
|
import Utils from './utils';
|
||||||
|
|
||||||
// Internationalisation.
|
// Internationalisation.
|
||||||
|
@ -18,46 +16,33 @@ Vue.use(Buefy, {});
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
// Globals.
|
// Globals.
|
||||||
const ut = new Utils(i18n);
|
Vue.prototype.$utils = new Utils(i18n);
|
||||||
Vue.mixin({
|
Vue.prototype.$api = api;
|
||||||
computed: {
|
|
||||||
$utils: () => ut,
|
|
||||||
$api: () => api,
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
$reloadServerConfig: () => {
|
|
||||||
// Get the config.js <script> tag, remove it, and re-add it.
|
|
||||||
let s = document.querySelector('#server-config');
|
|
||||||
const url = s.getAttribute('src');
|
|
||||||
s.remove();
|
|
||||||
|
|
||||||
s = document.createElement('script');
|
|
||||||
s.setAttribute('src', url);
|
|
||||||
s.setAttribute('id', 'server-config');
|
|
||||||
s.onload = () => {
|
|
||||||
store.commit('setModelResponse',
|
|
||||||
{ model: models.serverConfig, data: humps.camelizeKeys(window.CONFIG) });
|
|
||||||
};
|
|
||||||
document.body.appendChild(s);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// window.CONFIG is loaded from /api/config.js directly in a <script> tag.
|
|
||||||
if (window.CONFIG) {
|
|
||||||
store.commit('setModelResponse',
|
|
||||||
{ model: models.serverConfig, data: humps.camelizeKeys(window.CONFIG) });
|
|
||||||
|
|
||||||
// Load language.
|
|
||||||
i18n.locale = window.CONFIG.lang['_.code'];
|
|
||||||
i18n.setLocaleMessage(i18n.locale, window.CONFIG.lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
i18n,
|
i18n,
|
||||||
render: (h) => h(App),
|
render: (h) => h(App),
|
||||||
|
|
||||||
|
data: {
|
||||||
|
isLoaded: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
loadConfig() {
|
||||||
|
api.getServerConfig().then((data) => {
|
||||||
|
api.getLang(data.lang).then((lang) => {
|
||||||
|
i18n.locale = data.lang;
|
||||||
|
i18n.setLocaleMessage(i18n.locale, lang);
|
||||||
|
this.isLoaded = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.loadConfig();
|
||||||
|
api.getSettings();
|
||||||
|
},
|
||||||
}).$mount('#app');
|
}).$mount('#app');
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
<b-field :label="$tc('globals.terms.messenger')" label-position="on-border">
|
<b-field :label="$tc('globals.terms.messenger')" label-position="on-border">
|
||||||
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger"
|
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger"
|
||||||
:disabled="!canEdit" required>
|
:disabled="!canEdit" required>
|
||||||
<option v-for="m in serverConfig.messengers"
|
<option v-for="m in messengers"
|
||||||
:value="m" :key="m">{{ m }}</option>
|
:value="m" :key="m">{{ m }}</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
@ -196,7 +196,7 @@ export default Vue.extend({
|
||||||
form: {
|
form: {
|
||||||
name: '',
|
name: '',
|
||||||
subject: '',
|
subject: '',
|
||||||
fromEmail: window.CONFIG.fromEmail,
|
fromEmail: '',
|
||||||
templateId: 0,
|
templateId: 0,
|
||||||
lists: [],
|
lists: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -352,7 +352,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['serverConfig', 'loading', 'lists', 'templates']),
|
...mapState(['settings', 'loading', 'lists', 'templates']),
|
||||||
|
|
||||||
canEdit() {
|
canEdit() {
|
||||||
return this.isNew
|
return this.isNew
|
||||||
|
@ -374,6 +374,10 @@ export default Vue.extend({
|
||||||
|
|
||||||
return this.lists.results.filter((l) => this.selListIDs.indexOf(l.id) > -1);
|
return this.lists.results.filter((l) => this.selListIDs.indexOf(l.id) > -1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
messengers() {
|
||||||
|
return ['email', ...this.settings.messengers.map((m) => m.name)];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -383,6 +387,8 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.form.fromEmail = this.settings['app.from_email'];
|
||||||
|
|
||||||
const { id } = this.$route.params;
|
const { id } = this.$route.params;
|
||||||
|
|
||||||
// New campaign.
|
// New campaign.
|
||||||
|
|
|
@ -22,13 +22,12 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<template v-if="settings['app.enable_public_subscription_page']">
|
||||||
<template v-if="serverConfig.enablePublicSubscriptionPage">
|
|
||||||
<hr />
|
<hr />
|
||||||
<h4>{{ $t('forms.publicSubPage') }}</h4>
|
<h4>{{ $t('forms.publicSubPage') }}</h4>
|
||||||
<p>
|
<p>
|
||||||
<a :href="`${serverConfig.rootURL}/subscription/form`"
|
<a :href="`${settings['app.root_url']}/subscription/form`"
|
||||||
target="_blank">{{ serverConfig.rootURL }}/subscription/form</a>
|
target="_blank">{{ settings['app.root_url'] }}/subscription/form</a>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,7 +38,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- eslint-disable max-len -->
|
<!-- eslint-disable max-len -->
|
||||||
<pre v-if="checked.length > 0"><form method="post" action="{{ serverConfig.rootURL }}/subscription/form" class="listmonk-form">
|
<pre v-if="checked.length > 0"><form method="post" action="{{ settings['app.root_url'] }}/subscription/form" class="listmonk-form">
|
||||||
<div>
|
<div>
|
||||||
<h3>Subscribe</h3>
|
<h3>Subscribe</h3>
|
||||||
<p><input type="text" name="email" placeholder="{{ $t('subscribers.email') }}" /></p>
|
<p><input type="text" name="email" placeholder="{{ $t('subscribers.email') }}" /></p>
|
||||||
|
@ -79,7 +78,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['lists', 'loading', 'serverConfig']),
|
...mapState(['loading', 'lists', 'settings']),
|
||||||
|
|
||||||
publicLists() {
|
publicLists() {
|
||||||
if (!this.lists.results) {
|
if (!this.lists.results) {
|
||||||
|
|
|
@ -192,7 +192,7 @@ export default Vue.extend({
|
||||||
name: this.$t('lists.optinTo', { name: list.name }),
|
name: this.$t('lists.optinTo', { name: list.name }),
|
||||||
subject: this.$t('lists.confirmSub', { name: list.name }),
|
subject: this.$t('lists.confirmSub', { name: list.name }),
|
||||||
lists: [list.id],
|
lists: [list.id],
|
||||||
from_email: this.serverConfig.fromEmail,
|
from_email: this.settings['app.from_email'],
|
||||||
content_type: 'richtext',
|
content_type: 'richtext',
|
||||||
messenger: 'email',
|
messenger: 'email',
|
||||||
type: 'optin',
|
type: 'optin',
|
||||||
|
@ -206,7 +206,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['serverConfig', 'loading', 'lists']),
|
...mapState(['loading', 'lists', 'settings']),
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<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 v-if="media.length > 0">({{ media.length }})</span>
|
||||||
|
|
||||||
<span class="has-text-grey-light"> / {{ serverConfig.mediaProvider }}</span>
|
<span class="has-text-grey-light"> / {{ settings['upload.provider'] }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<b-loading :active="isProcessing || loading.media"></b-loading>
|
<b-loading :active="isProcessing || loading.media"></b-loading>
|
||||||
|
@ -141,7 +141,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['media', 'serverConfig', 'loading']),
|
...mapState(['loading', 'media', 'settings']),
|
||||||
|
|
||||||
isProcessing() {
|
isProcessing() {
|
||||||
if (this.toUpload > 0 && this.uploaded < this.toUpload) {
|
if (this.toUpload > 0 && this.uploaded < this.toUpload) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="settings">
|
<section class="settings">
|
||||||
<b-loading :is-full-page="true" v-if="isLoading" active />
|
<b-loading :is-full-page="true" v-if="loading.settings || isLoading" active />
|
||||||
<header class="columns">
|
<header class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<h1 class="title is-4">{{ $t('settings.title') }}</h1>
|
<h1 class="title is-4">{{ $t('settings.title') }}</h1>
|
||||||
|
@ -528,8 +528,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import store from '../store';
|
|
||||||
import { models } from '../constants';
|
|
||||||
|
|
||||||
const dummyPassword = ' '.repeat(8);
|
const dummyPassword = ' '.repeat(8);
|
||||||
|
|
||||||
|
@ -537,7 +535,7 @@ export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
regDuration: '[0-9]+(ms|s|m|h|d)',
|
regDuration: '[0-9]+(ms|s|m|h|d)',
|
||||||
isLoading: true,
|
isLoading: false,
|
||||||
|
|
||||||
// formCopy is a stringified copy of the original settings against which
|
// formCopy is a stringified copy of the original settings against which
|
||||||
// form is compared to detect changes.
|
// form is compared to detect changes.
|
||||||
|
@ -635,11 +633,11 @@ export default Vue.extend({
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.$api.updateSettings(form).then((data) => {
|
this.$api.updateSettings(form).then((data) => {
|
||||||
if (data.needsRestart) {
|
if (data.needsRestart) {
|
||||||
// Update the 'needsRestart' flag on the global serverConfig state
|
// There are running campaigns and the app didn't auto restart.
|
||||||
// as there are running campaigns and the app couldn't auto-restart.
|
// The UI will show a warning.
|
||||||
store.commit('setModelResponse',
|
this.$root.loadConfig();
|
||||||
{ model: models.serverConfig, data: { ...this.serverConfig, needsRestart: true } });
|
|
||||||
this.getSettings();
|
this.getSettings();
|
||||||
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -650,8 +648,8 @@ export default Vue.extend({
|
||||||
const pollId = setInterval(() => {
|
const pollId = setInterval(() => {
|
||||||
this.$api.getHealth().then(() => {
|
this.$api.getHealth().then(() => {
|
||||||
clearInterval(pollId);
|
clearInterval(pollId);
|
||||||
|
this.$root.loadConfig();
|
||||||
this.getSettings();
|
this.getSettings();
|
||||||
this.$reloadServerConfig();
|
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@ -666,7 +664,7 @@ export default Vue.extend({
|
||||||
for (let i = 0; i < d.smtp.length; i += 1) {
|
for (let i = 0; i < d.smtp.length; i += 1) {
|
||||||
d.smtp[i].strEmailHeaders = JSON.stringify(d.smtp[i].email_headers, null, 4);
|
d.smtp[i].strEmailHeaders = JSON.stringify(d.smtp[i].email_headers, null, 4);
|
||||||
|
|
||||||
// The backend doesn't send passwords, so add a dummy so that it
|
// The backend doesn't send passwords, so add a dummy so that
|
||||||
// the password looks filled on the UI.
|
// the password looks filled on the UI.
|
||||||
d.smtp[i].password = dummyPassword;
|
d.smtp[i].password = dummyPassword;
|
||||||
}
|
}
|
||||||
|
|
|
@ -271,6 +271,10 @@
|
||||||
"public.unsubbedInfo": "You have unsubscribed successfully.",
|
"public.unsubbedInfo": "You have unsubscribed successfully.",
|
||||||
"public.unsubbedTitle": "Unsubscribed",
|
"public.unsubbedTitle": "Unsubscribed",
|
||||||
"public.unsubscribeTitle": "Unsubscribe from mailing list",
|
"public.unsubscribeTitle": "Unsubscribe from mailing list",
|
||||||
|
"settings.needsRestart": "Settings changed. Pause all running campaigns and restart the app",
|
||||||
|
"settings.confirmRestart": "Ensure running campaigns are paused. Restart?",
|
||||||
|
"settings.updateAvailable": "A new update {version} is available.",
|
||||||
|
"settings.restart": "Restart",
|
||||||
"settings.duplicateMessengerName": "Duplicate messenger name: {name}",
|
"settings.duplicateMessengerName": "Duplicate messenger name: {name}",
|
||||||
"settings.errorEncoding": "Error encoding settings: {error}",
|
"settings.errorEncoding": "Error encoding settings: {error}",
|
||||||
"settings.errorNoSMTP": "At least one SMTP block should be enabled",
|
"settings.errorNoSMTP": "At least one SMTP block should be enabled",
|
||||||
|
|
Loading…
Reference in New Issue