mirror of
https://github.com/knadh/listmonk.git
synced 2024-09-20 07:16:33 +08:00
WIP: Add support for publishing campaigns to publish archives.
This commit is contained in:
parent
74322cda36
commit
9add728b08
172
cmd/archive.go
Normal file
172
cmd/archive.go
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/knadh/listmonk/internal/manager"
|
||||||
|
"github.com/knadh/listmonk/models"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
null "gopkg.in/volatiletech/null.v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type campArchive struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
CreatedAt null.Time `json:"created_at"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetCampaignArchives renders the public campaign archives page.
|
||||||
|
func handleGetCampaignArchives(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
pg = getPagination(c.QueryParams(), 50)
|
||||||
|
)
|
||||||
|
|
||||||
|
camps, total, err := getCampaignArchives(pg.Offset, pg.Limit, app)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out models.PageResults
|
||||||
|
if len(camps) == 0 {
|
||||||
|
out.Results = []campArchive{}
|
||||||
|
return c.JSON(http.StatusOK, okResp{out})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
out.Results = camps
|
||||||
|
out.Total = total
|
||||||
|
out.Page = pg.Page
|
||||||
|
out.PerPage = pg.PerPage
|
||||||
|
|
||||||
|
return c.JSON(200, okResp{out})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCampaignArchivesPage renders the public campaign archives page.
|
||||||
|
func handleCampaignArchivesPage(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
pg = getPagination(c.QueryParams(), 50)
|
||||||
|
)
|
||||||
|
|
||||||
|
out, total, err := getCampaignArchives(pg.Offset, pg.Limit, app)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
title := app.i18n.T("public.archiveTitle")
|
||||||
|
return c.Render(http.StatusOK, "archive", struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Campaigns []campArchive
|
||||||
|
Total int
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
}{title, title, out, total, pg.Page, pg.PerPage})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCampaignArchivePage renders the public campaign archives page.
|
||||||
|
func handleCampaignArchivePage(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
uuid = c.Param("uuid")
|
||||||
|
)
|
||||||
|
|
||||||
|
pubCamp, err := app.core.GetArchivedCampaign(0, uuid)
|
||||||
|
if err != nil {
|
||||||
|
if er, ok := err.(*echo.HTTPError); ok {
|
||||||
|
if er.Code == http.StatusBadRequest {
|
||||||
|
return c.Render(http.StatusNotFound, tplMessage,
|
||||||
|
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", app.i18n.T("public.campaignNotFound")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
|
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := compileArchiveCampaigns([]models.Campaign{pubCamp}, app)
|
||||||
|
if err != nil {
|
||||||
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
|
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the message body.
|
||||||
|
camp := out[0].Campaign
|
||||||
|
msg, err := app.manager.NewCampaignMessage(camp, out[0].Subscriber)
|
||||||
|
if err != nil {
|
||||||
|
app.log.Printf("error rendering message: %v", err)
|
||||||
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
|
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.HTML(http.StatusOK, string(msg.Body()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCampaignArchives(offset, limit int, app *App) ([]campArchive, int, error) {
|
||||||
|
pubCamps, total, err := app.core.GetArchivedCampaigns(offset, limit)
|
||||||
|
if err != nil {
|
||||||
|
return []campArchive{}, total, echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("public.errorFetchingCampaign"))
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, err := compileArchiveCampaigns(pubCamps, app)
|
||||||
|
if err != nil {
|
||||||
|
return []campArchive{}, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]campArchive, 0, len(msgs))
|
||||||
|
for _, m := range msgs {
|
||||||
|
camp := m.Campaign
|
||||||
|
out = append(out, campArchive{
|
||||||
|
UUID: camp.UUID,
|
||||||
|
Subject: camp.Subject,
|
||||||
|
CreatedAt: camp.CreatedAt,
|
||||||
|
URL: app.constants.ArchiveURL + "/" + camp.UUID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileArchiveCampaigns(camps []models.Campaign, app *App) ([]manager.CampaignMessage, error) {
|
||||||
|
var (
|
||||||
|
b = bytes.Buffer{}
|
||||||
|
)
|
||||||
|
|
||||||
|
out := make([]manager.CampaignMessage, 0, len(camps))
|
||||||
|
for _, camp := range camps {
|
||||||
|
if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
||||||
|
app.log.Printf("error compiling template: %v", err)
|
||||||
|
return nil, echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("public.errorFetchingCampaign"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the dummy subscriber meta.
|
||||||
|
var sub models.Subscriber
|
||||||
|
if err := json.Unmarshal([]byte(camp.ArchiveMeta), &sub); err != nil {
|
||||||
|
app.log.Printf("error unmarshalling campaign archive meta: %v", err)
|
||||||
|
return nil, echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("public.errorFetchingCampaign"))
|
||||||
|
}
|
||||||
|
|
||||||
|
m := manager.CampaignMessage{
|
||||||
|
Campaign: &camp,
|
||||||
|
Subscriber: sub,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the subject if it's a template.
|
||||||
|
if camp.SubjectTpl != nil {
|
||||||
|
if err := camp.SubjectTpl.ExecuteTemplate(&b, models.ContentTpl, m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
camp.Subject = b.String()
|
||||||
|
b.Reset()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
@ -215,6 +216,10 @@ func handleCreateCampaign(c echo.Context) error {
|
||||||
o = c
|
o = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.ArchiveTemplateID == 0 {
|
||||||
|
o.ArchiveTemplateID = o.TemplateID
|
||||||
|
}
|
||||||
|
|
||||||
out, err := app.core.CreateCampaign(o.Campaign, o.ListIDs)
|
out, err := app.core.CreateCampaign(o.Campaign, o.ListIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -294,6 +299,31 @@ func handleUpdateCampaignStatus(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, okResp{out})
|
return c.JSON(http.StatusOK, okResp{out})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleUpdateCampaignArchive handles campaign status modification.
|
||||||
|
func handleUpdateCampaignArchive(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
|
)
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Archive bool `json:"archive"`
|
||||||
|
TemplateID int `json:"archive_template_id"`
|
||||||
|
Meta models.JSON `json:"archive_meta"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// Get and validate fields.
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.core.UpdateCampaignArchive(id, req.Archive, req.TemplateID, req.Meta); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, okResp{true})
|
||||||
|
}
|
||||||
|
|
||||||
// handleDeleteCampaign handles campaign deletion.
|
// handleDeleteCampaign handles campaign deletion.
|
||||||
// Only scheduled campaigns that have not started yet can be deleted.
|
// Only scheduled campaigns that have not started yet can be deleted.
|
||||||
func handleDeleteCampaign(c echo.Context) error {
|
func handleDeleteCampaign(c echo.Context) error {
|
||||||
|
@ -529,6 +559,10 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
|
||||||
c.Headers = make([]map[string]string, 0)
|
c.Headers = make([]map[string]string, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(c.ArchiveMeta) == 0 {
|
||||||
|
c.ArchiveMeta = json.RawMessage("{}")
|
||||||
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,6 +131,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
g.POST("/api/campaigns", handleCreateCampaign)
|
g.POST("/api/campaigns", handleCreateCampaign)
|
||||||
g.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
g.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
||||||
g.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
|
g.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
|
||||||
|
g.PUT("/api/campaigns/:id/archive", handleUpdateCampaignArchive)
|
||||||
g.DELETE("/api/campaigns/:id", handleDeleteCampaign)
|
g.DELETE("/api/campaigns/:id", handleDeleteCampaign)
|
||||||
|
|
||||||
g.GET("/api/media", handleGetMedia)
|
g.GET("/api/media", handleGetMedia)
|
||||||
|
@ -164,6 +165,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
// Public API endpoints.
|
// Public API endpoints.
|
||||||
e.GET("/api/public/lists", handleGetPublicLists)
|
e.GET("/api/public/lists", handleGetPublicLists)
|
||||||
e.POST("/api/public/subscription", handlePublicSubscription)
|
e.POST("/api/public/subscription", handlePublicSubscription)
|
||||||
|
e.GET("/api/public/archive", handleGetCampaignArchives)
|
||||||
|
|
||||||
// /public/static/* file server is registered in initHTTPServer().
|
// /public/static/* file server is registered in initHTTPServer().
|
||||||
// Public subscriber facing views.
|
// Public subscriber facing views.
|
||||||
|
@ -185,6 +187,8 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
"campUUID", "subUUID")))
|
"campUUID", "subUUID")))
|
||||||
e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
|
e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
|
||||||
"campUUID", "subUUID")))
|
"campUUID", "subUUID")))
|
||||||
|
e.GET("/archive", handleCampaignArchivesPage)
|
||||||
|
e.GET("/archive/:uuid", handleCampaignArchivePage)
|
||||||
|
|
||||||
e.GET("/public/custom.css", serveCustomApperance("public.custom_css"))
|
e.GET("/public/custom.css", serveCustomApperance("public.custom_css"))
|
||||||
e.GET("/public/custom.js", serveCustomApperance("public.custom_js"))
|
e.GET("/public/custom.js", serveCustomApperance("public.custom_js"))
|
||||||
|
|
|
@ -82,6 +82,7 @@ type constants struct {
|
||||||
ViewTrackURL string
|
ViewTrackURL string
|
||||||
OptinURL string
|
OptinURL string
|
||||||
MessageURL string
|
MessageURL string
|
||||||
|
ArchiveURL string
|
||||||
MediaProvider string
|
MediaProvider string
|
||||||
|
|
||||||
BounceWebhooksEnabled bool
|
BounceWebhooksEnabled bool
|
||||||
|
@ -370,6 +371,9 @@ func initConstants() *constants {
|
||||||
// url.com/link/{campaign_uuid}/{subscriber_uuid}
|
// url.com/link/{campaign_uuid}/{subscriber_uuid}
|
||||||
c.MessageURL = fmt.Sprintf("%s/campaign/%%s/%%s", c.RootURL)
|
c.MessageURL = fmt.Sprintf("%s/campaign/%%s/%%s", c.RootURL)
|
||||||
|
|
||||||
|
// url.com/archive
|
||||||
|
c.ArchiveURL = c.RootURL + "/archive"
|
||||||
|
|
||||||
// url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
|
// url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
|
||||||
c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL)
|
c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL)
|
||||||
|
|
||||||
|
@ -424,6 +428,7 @@ func initCampaignManager(q *models.Queries, cs *constants, app *App) *manager.Ma
|
||||||
LinkTrackURL: cs.LinkTrackURL,
|
LinkTrackURL: cs.LinkTrackURL,
|
||||||
ViewTrackURL: cs.ViewTrackURL,
|
ViewTrackURL: cs.ViewTrackURL,
|
||||||
MessageURL: cs.MessageURL,
|
MessageURL: cs.MessageURL,
|
||||||
|
ArchiveURL: cs.ArchiveURL,
|
||||||
UnsubHeader: ko.Bool("privacy.unsubscribe_header"),
|
UnsubHeader: ko.Bool("privacy.unsubscribe_header"),
|
||||||
SlidingWindow: ko.Bool("app.message_sliding_window"),
|
SlidingWindow: ko.Bool("app.message_sliding_window"),
|
||||||
SlidingWindowDuration: ko.Duration("app.message_sliding_window_duration"),
|
SlidingWindowDuration: ko.Duration("app.message_sliding_window_duration"),
|
||||||
|
|
|
@ -147,6 +147,9 @@ func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempo
|
||||||
emailMsgr,
|
emailMsgr,
|
||||||
campTplID,
|
campTplID,
|
||||||
pq.Int64Array{1},
|
pq.Int64Array{1},
|
||||||
|
false,
|
||||||
|
campTplID,
|
||||||
|
"{}",
|
||||||
); err != nil {
|
); err != nil {
|
||||||
lo.Fatalf("error creating sample campaign: %v", err)
|
lo.Fatalf("error creating sample campaign: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ func (r *runnerDB) NextSubscribers(campID, limit int) ([]models.Subscriber, erro
|
||||||
// GetCampaign fetches a campaign from the database.
|
// GetCampaign fetches a campaign from the database.
|
||||||
func (r *runnerDB) GetCampaign(campID int) (*models.Campaign, error) {
|
func (r *runnerDB) GetCampaign(campID int) (*models.Campaign, error) {
|
||||||
var out = &models.Campaign{}
|
var out = &models.Campaign{}
|
||||||
err := r.queries.GetCampaign.Get(out, campID, nil)
|
err := r.queries.GetCampaign.Get(out, campID, nil, "default")
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,6 @@ func handleViewCampaignMessage(c echo.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.log.Printf("error fetching campaign: %v", err)
|
|
||||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||||
}
|
}
|
||||||
|
|
52
frontend/cypress/e2e/archive.cy.js
Normal file
52
frontend/cypress/e2e/archive.cy.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
const apiUrl = Cypress.env('apiUrl');
|
||||||
|
|
||||||
|
describe('Archive', () => {
|
||||||
|
it('Opens campaigns page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/campaigns');
|
||||||
|
cy.wait(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clones campaign', () => {
|
||||||
|
cy.loginAndVisit('/campaigns');
|
||||||
|
cy.get('[data-cy=btn-clone]').first().click();
|
||||||
|
cy.get('.modal input').clear().type('clone').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.clickMenu('all-campaigns');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Starts un-archived campaign', () => {
|
||||||
|
cy.get('td[data-label=Status] a').eq(0).click();
|
||||||
|
cy.get('[data-cy=btn-start]').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Enables archive on one campaign', () => {
|
||||||
|
cy.loginAndVisit('/campaigns');
|
||||||
|
cy.wait(250);
|
||||||
|
cy.get('td[data-label=Status] a').eq(1).click();
|
||||||
|
|
||||||
|
// Switch to archive tab and enable archive.
|
||||||
|
cy.get('.b-tabs nav a').eq(2).click();
|
||||||
|
cy.wait(500);
|
||||||
|
cy.get('[data-cy=btn-archive] .check').click();
|
||||||
|
cy.get('[data-cy=archive-meta]').clear()
|
||||||
|
.type('{"email": "archive@domain.com", "name": "Archive", "attribs": { "city": "Bengaluru"}}', { 'parseSpecialCharSequences': false });
|
||||||
|
|
||||||
|
// Start the campaign.
|
||||||
|
cy.get('[data-cy=btn-save]').click();
|
||||||
|
cy.wait(500);
|
||||||
|
cy.get('[data-cy=btn-start]').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Opens campaign archive page', () => {
|
||||||
|
cy.loginAndVisit(`${apiUrl}/archive`);
|
||||||
|
cy.get('li a').click();
|
||||||
|
cy.get('h3').contains('Hi Archive!');
|
||||||
|
cy.get('p').eq(0).contains('Bengaluru');
|
||||||
|
});
|
||||||
|
});
|
|
@ -238,6 +238,9 @@ export const updateCampaign = async (id, data) => http.put(`/api/campaigns/${id}
|
||||||
export const changeCampaignStatus = async (id, status) => http.put(`/api/campaigns/${id}/status`,
|
export const changeCampaignStatus = async (id, status) => http.put(`/api/campaigns/${id}/status`,
|
||||||
{ status }, { loading: models.campaigns });
|
{ status }, { 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}`,
|
export const deleteCampaign = async (id) => http.delete(`/api/campaigns/${id}`,
|
||||||
{ loading: models.campaigns });
|
{ loading: models.campaigns });
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
<b-tabs type="is-boxed" :animated="false" v-model="activeTab" @input="onTab">
|
<b-tabs type="is-boxed" :animated="false" v-model="activeTab" @input="onTab">
|
||||||
<b-tab-item :label="$tc('globals.terms.campaign')" label-position="on-border"
|
<b-tab-item :label="$tc('globals.terms.campaign')" label-position="on-border"
|
||||||
icon="rocket-launch-outline">
|
value="campaign" icon="rocket-launch-outline">
|
||||||
<section class="wrap">
|
<section class="wrap">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
</section>
|
</section>
|
||||||
</b-tab-item><!-- campaign -->
|
</b-tab-item><!-- campaign -->
|
||||||
|
|
||||||
<b-tab-item :label="$t('campaigns.content')" icon="text" :disabled="isNew">
|
<b-tab-item :label="$t('campaigns.content')" icon="text" :disabled="isNew" value="content">
|
||||||
<editor
|
<editor
|
||||||
v-model="form.content"
|
v-model="form.content"
|
||||||
:id="data.id"
|
:id="data.id"
|
||||||
|
@ -198,6 +198,39 @@
|
||||||
type="textarea" :disabled="!canEdit" />
|
type="textarea" :disabled="!canEdit" />
|
||||||
</div>
|
</div>
|
||||||
</b-tab-item><!-- content -->
|
</b-tab-item><!-- content -->
|
||||||
|
|
||||||
|
<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-switch data-cy="btn-archive" v-model="form.archive" :disabled="!canArchive" />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="$tc('globals.terms.template')" label-position="on-border">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
{{ $t('globals.buttons.saveChanges') }}
|
||||||
|
</b-button>
|
||||||
|
</b-field>
|
||||||
|
</section>
|
||||||
|
</b-tab-item><!-- archive -->
|
||||||
</b-tabs>
|
</b-tabs>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
@ -211,6 +244,8 @@ import htmlToPlainText from 'textversionjs';
|
||||||
import ListSelector from '../components/ListSelector.vue';
|
import ListSelector from '../components/ListSelector.vue';
|
||||||
import Editor from '../components/Editor.vue';
|
import Editor from '../components/Editor.vue';
|
||||||
|
|
||||||
|
const TABS = ['campaign', 'content', 'archive'];
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
ListSelector,
|
ListSelector,
|
||||||
|
@ -247,7 +282,9 @@ export default Vue.extend({
|
||||||
// Parsed Date() version of send_at from the API.
|
// Parsed Date() version of send_at from the API.
|
||||||
sendAtDate: null,
|
sendAtDate: null,
|
||||||
sendLater: false,
|
sendLater: false,
|
||||||
|
archive: false,
|
||||||
|
archiveMetaStr: '{}',
|
||||||
|
archiveMeta: {},
|
||||||
testEmails: [],
|
testEmails: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -276,7 +313,8 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
onTab(t) {
|
onTab(t) {
|
||||||
if (t === 1 && window.tinymce && window.tinymce.editors.length > 0) {
|
const tab = TABS[t];
|
||||||
|
if (tab === 'content' && window.tinymce && window.tinymce.editors.length > 0) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
window.tinymce.editors[0].focus();
|
window.tinymce.editors[0].focus();
|
||||||
});
|
});
|
||||||
|
@ -284,6 +322,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit(typ) {
|
onSubmit(typ) {
|
||||||
|
// Validate custom JSON headers.
|
||||||
if (this.form.headersStr && this.form.headersStr !== '[]') {
|
if (this.form.headersStr && this.form.headersStr !== '[]') {
|
||||||
try {
|
try {
|
||||||
this.form.headers = JSON.parse(this.form.headersStr);
|
this.form.headers = JSON.parse(this.form.headersStr);
|
||||||
|
@ -295,6 +334,23 @@ export default Vue.extend({
|
||||||
this.form.headers = [];
|
this.form.headers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate archive JSON body.
|
||||||
|
if (this.form.archive && this.form.archiveMetaStr) {
|
||||||
|
try {
|
||||||
|
this.form.archiveMeta = JSON.parse(this.form.archiveMetaStr);
|
||||||
|
} catch (e) {
|
||||||
|
this.$utils.toast(e.toString(), 'is-danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.form.archiveMeta = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the campaign archive metadata for the next one.
|
||||||
|
if (this.isEditing) {
|
||||||
|
this.$utils.setPref('campaign.archiveMetaStr', this.form.archiveMetaStr);
|
||||||
|
}
|
||||||
|
|
||||||
switch (typ) {
|
switch (typ) {
|
||||||
case 'create':
|
case 'create':
|
||||||
this.createCampaign();
|
this.createCampaign();
|
||||||
|
@ -315,11 +371,17 @@ export default Vue.extend({
|
||||||
...this.form,
|
...this.form,
|
||||||
...data,
|
...data,
|
||||||
headersStr: JSON.stringify(data.headers, null, 4),
|
headersStr: JSON.stringify(data.headers, null, 4),
|
||||||
|
archiveMetaStr: data.archiveMeta ? JSON.stringify(data.archiveMeta, null, 4) : '{}',
|
||||||
|
|
||||||
// The structure that is populated by editor input event.
|
// The structure that is populated by editor input event.
|
||||||
content: { contentType: data.contentType, body: data.body },
|
content: { contentType: data.contentType, body: data.body },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.form.archiveMetaStr === '{}') {
|
||||||
|
const archiveStr = `{"email": "email@domain.com", "name": "${this.$t('globals.fields.name')}", "attribs": {}}`;
|
||||||
|
this.form.archiveMetaStr = this.$utils.getPref('campaign.archiveMetaStr') || JSON.stringify(JSON.parse(archiveStr), null, 4);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.sendAt !== null) {
|
if (data.sendAt !== null) {
|
||||||
this.form.sendLater = true;
|
this.form.sendLater = true;
|
||||||
this.form.sendAtDate = dayjs(data.sendAt).toDate();
|
this.form.sendAtDate = dayjs(data.sendAt).toDate();
|
||||||
|
@ -390,6 +452,9 @@ export default Vue.extend({
|
||||||
content_type: this.form.content.contentType,
|
content_type: this.form.content.contentType,
|
||||||
body: this.form.content.body,
|
body: this.form.content.body,
|
||||||
altbody: this.form.content.contentType !== 'plain' ? this.form.altbody : null,
|
altbody: this.form.content.contentType !== 'plain' ? this.form.altbody : null,
|
||||||
|
archive: this.form.archive,
|
||||||
|
archive_template_id: this.form.archiveTemplateId,
|
||||||
|
archive_meta: this.form.archiveMeta,
|
||||||
};
|
};
|
||||||
|
|
||||||
let typMsg = 'globals.messages.updated';
|
let typMsg = 'globals.messages.updated';
|
||||||
|
@ -407,6 +472,20 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onUpdateCampaignArchive() {
|
||||||
|
if (this.isEditing && this.canEdit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
archive: this.form.archive,
|
||||||
|
archive_template_id: this.form.archiveTemplateId,
|
||||||
|
archive_meta: JSON.parse(this.form.archiveMetaStr),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$api.updateCampaignArchive(this.data.id, data);
|
||||||
|
},
|
||||||
|
|
||||||
// Starts or schedule a campaign.
|
// Starts or schedule a campaign.
|
||||||
startCampaign() {
|
startCampaign() {
|
||||||
if (!this.canStart && !this.canSchedule) {
|
if (!this.canStart && !this.canSchedule) {
|
||||||
|
@ -451,6 +530,10 @@ export default Vue.extend({
|
||||||
return this.data.status === 'draft' && !this.data.sendAt;
|
return this.data.status === 'draft' && !this.data.sendAt;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canArchive() {
|
||||||
|
return this.data.status !== 'cancelled';
|
||||||
|
},
|
||||||
|
|
||||||
selectedLists() {
|
selectedLists() {
|
||||||
if (this.selListIDs.length === 0 || !this.lists.results) {
|
if (this.selListIDs.length === 0 || !this.lists.results) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -481,11 +564,11 @@ export default Vue.extend({
|
||||||
mounted() {
|
mounted() {
|
||||||
window.onbeforeunload = () => this.isUnsaved() || null;
|
window.onbeforeunload = () => this.isUnsaved() || null;
|
||||||
|
|
||||||
|
// Fill default form fields.
|
||||||
this.form.fromEmail = this.settings['app.from_email'];
|
this.form.fromEmail = this.settings['app.from_email'];
|
||||||
|
|
||||||
const { id } = this.$route.params;
|
|
||||||
|
|
||||||
// New campaign.
|
// New campaign.
|
||||||
|
const { id } = this.$route.params;
|
||||||
if (id === 'new') {
|
if (id === 'new') {
|
||||||
this.isNew = true;
|
this.isNew = true;
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Servei desconegut",
|
"bounces.unknownService": "Servei desconegut",
|
||||||
"bounces.view": "Veure rebots",
|
"bounces.view": "Veure rebots",
|
||||||
"campaigns.addAltText": "Afegeix un missatge de text pla alternatiu",
|
"campaigns.addAltText": "Afegeix un missatge de text pla alternatiu",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "No es pot actualitzar una campanya en curs o ja finalitzada.",
|
"campaigns.cantUpdate": "No es pot actualitzar una campanya en curs o ja finalitzada.",
|
||||||
"campaigns.clicks": "Clics",
|
"campaigns.clicks": "Clics",
|
||||||
"campaigns.confirmDelete": "Esborra {name}",
|
"campaigns.confirmDelete": "Esborra {name}",
|
||||||
|
@ -281,6 +286,8 @@
|
||||||
"menu.media": "Mèdia",
|
"menu.media": "Mèdia",
|
||||||
"menu.newCampaign": "Crea nova",
|
"menu.newCampaign": "Crea nova",
|
||||||
"menu.settings": "Configuració",
|
"menu.settings": "Configuració",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "No s'ha trobat el missatge de correu electrònic.",
|
"public.campaignNotFound": "No s'ha trobat el missatge de correu electrònic.",
|
||||||
"public.confirmOptinSubTitle": "Confirmació de la subscripció",
|
"public.confirmOptinSubTitle": "Confirmació de la subscripció",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Neznámá služba.",
|
"bounces.unknownService": "Neznámá služba.",
|
||||||
"bounces.view": "Zobrazit převzetí",
|
"bounces.view": "Zobrazit převzetí",
|
||||||
"campaigns.addAltText": "Přidat alternativní zprávu ve formátu prostého textu",
|
"campaigns.addAltText": "Přidat alternativní zprávu ve formátu prostého textu",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Nelze aktualizovat spuštěnou nebo dokončenou kampaň.",
|
"campaigns.cantUpdate": "Nelze aktualizovat spuštěnou nebo dokončenou kampaň.",
|
||||||
"campaigns.clicks": "Klepnutí",
|
"campaigns.clicks": "Klepnutí",
|
||||||
"campaigns.confirmDelete": "Odstranit {name}",
|
"campaigns.confirmDelete": "Odstranit {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Médium",
|
"menu.media": "Médium",
|
||||||
"menu.newCampaign": "Vytvořit nový",
|
"menu.newCampaign": "Vytvořit nový",
|
||||||
"menu.settings": "Nastavení",
|
"menu.settings": "Nastavení",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "E-mailová zpráva nebyla nalezena.",
|
"public.campaignNotFound": "E-mailová zpráva nebyla nalezena.",
|
||||||
"public.confirmOptinSubTitle": "Potvrdit odběr",
|
"public.confirmOptinSubTitle": "Potvrdit odběr",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Unbekannter Dienst.",
|
"bounces.unknownService": "Unbekannter Dienst.",
|
||||||
"bounces.view": "Bounces anzeigen",
|
"bounces.view": "Bounces anzeigen",
|
||||||
"campaigns.addAltText": "Füge eine alternative Nachricht in unformatierten Text hinzu (falls HTML nicht angezeigt werden kann).",
|
"campaigns.addAltText": "Füge eine alternative Nachricht in unformatierten Text hinzu (falls HTML nicht angezeigt werden kann).",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Eine laufende oder abgeschlossene Kampagne kann nicht geändert werden.",
|
"campaigns.cantUpdate": "Eine laufende oder abgeschlossene Kampagne kann nicht geändert werden.",
|
||||||
"campaigns.clicks": "Klicks",
|
"campaigns.clicks": "Klicks",
|
||||||
"campaigns.confirmDelete": "Lösche {name}",
|
"campaigns.confirmDelete": "Lösche {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Medien",
|
"menu.media": "Medien",
|
||||||
"menu.newCampaign": "Neu Anlegen",
|
"menu.newCampaign": "Neu Anlegen",
|
||||||
"menu.settings": "Einstellungen",
|
"menu.settings": "Einstellungen",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Die E-Mail wurde nicht gefunden.",
|
"public.campaignNotFound": "Die E-Mail wurde nicht gefunden.",
|
||||||
"public.confirmOptinSubTitle": "Abonnement bestätigen",
|
"public.confirmOptinSubTitle": "Abonnement bestätigen",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Unknown service.",
|
"bounces.unknownService": "Unknown service.",
|
||||||
"bounces.view": "View bounces",
|
"bounces.view": "View bounces",
|
||||||
"campaigns.addAltText": "Add alternate plain text message",
|
"campaigns.addAltText": "Add alternate plain text message",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Cannot update a running or a finished campaign.",
|
"campaigns.cantUpdate": "Cannot update a running or a finished campaign.",
|
||||||
"campaigns.clicks": "Clicks",
|
"campaigns.clicks": "Clicks",
|
||||||
"campaigns.confirmDelete": "Delete {name}",
|
"campaigns.confirmDelete": "Delete {name}",
|
||||||
|
@ -281,6 +286,8 @@
|
||||||
"menu.media": "Media",
|
"menu.media": "Media",
|
||||||
"menu.newCampaign": "Create new",
|
"menu.newCampaign": "Create new",
|
||||||
"menu.settings": "Settings",
|
"menu.settings": "Settings",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "The e-mail message was not found.",
|
"public.campaignNotFound": "The e-mail message was not found.",
|
||||||
"public.confirmOptinSubTitle": "Confirm subscription",
|
"public.confirmOptinSubTitle": "Confirm subscription",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Servicio desconocido.",
|
"bounces.unknownService": "Servicio desconocido.",
|
||||||
"bounces.view": "Ver rebotes",
|
"bounces.view": "Ver rebotes",
|
||||||
"campaigns.addAltText": "Agregar mensaje en texto plano alternativo",
|
"campaigns.addAltText": "Agregar mensaje en texto plano alternativo",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "No es posible actualizar una campaña iniciada o finalizada.",
|
"campaigns.cantUpdate": "No es posible actualizar una campaña iniciada o finalizada.",
|
||||||
"campaigns.clicks": "Clics",
|
"campaigns.clicks": "Clics",
|
||||||
"campaigns.confirmDelete": "Eliminar {name}",
|
"campaigns.confirmDelete": "Eliminar {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Multimedia",
|
"menu.media": "Multimedia",
|
||||||
"menu.newCampaign": "Crear nueva",
|
"menu.newCampaign": "Crear nueva",
|
||||||
"menu.settings": "Configuraciones",
|
"menu.settings": "Configuraciones",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "El mensaje de correo electrónico no fue encontrado",
|
"public.campaignNotFound": "El mensaje de correo electrónico no fue encontrado",
|
||||||
"public.confirmOptinSubTitle": "Confirmar subscripción",
|
"public.confirmOptinSubTitle": "Confirmar subscripción",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Tuntematon palvelu.",
|
"bounces.unknownService": "Tuntematon palvelu.",
|
||||||
"bounces.view": "Näytä epäonnistuneet toimitukset",
|
"bounces.view": "Näytä epäonnistuneet toimitukset",
|
||||||
"campaigns.addAltText": "Lisää vaihtoehtoinen tekstimuotoinen viesti",
|
"campaigns.addAltText": "Lisää vaihtoehtoinen tekstimuotoinen viesti",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Käynnissä olevaa tai päättynyttä kampanjaa ei voi päivittää.",
|
"campaigns.cantUpdate": "Käynnissä olevaa tai päättynyttä kampanjaa ei voi päivittää.",
|
||||||
"campaigns.clicks": "Klikkaukset",
|
"campaigns.clicks": "Klikkaukset",
|
||||||
"campaigns.confirmDelete": "Poista {name}",
|
"campaigns.confirmDelete": "Poista {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Media",
|
"menu.media": "Media",
|
||||||
"menu.newCampaign": "Create new",
|
"menu.newCampaign": "Create new",
|
||||||
"menu.settings": "Settings",
|
"menu.settings": "Settings",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Sähköpostiviestiä ei löytynyt",
|
"public.campaignNotFound": "Sähköpostiviestiä ei löytynyt",
|
||||||
"public.confirmOptinSubTitle": "Vahvista uutiskirjeen tilaus",
|
"public.confirmOptinSubTitle": "Vahvista uutiskirjeen tilaus",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Service inconnu.",
|
"bounces.unknownService": "Service inconnu.",
|
||||||
"bounces.view": "Voir les rebonds",
|
"bounces.view": "Voir les rebonds",
|
||||||
"campaigns.addAltText": "Ajouter un message alternatif en texte brut",
|
"campaigns.addAltText": "Ajouter un message alternatif en texte brut",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Impossible de mettre à jour une campagne en cours ou terminée.",
|
"campaigns.cantUpdate": "Impossible de mettre à jour une campagne en cours ou terminée.",
|
||||||
"campaigns.clicks": "Clics",
|
"campaigns.clicks": "Clics",
|
||||||
"campaigns.confirmDelete": "Supprimer la campagne {name}",
|
"campaigns.confirmDelete": "Supprimer la campagne {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Fichiers",
|
"menu.media": "Fichiers",
|
||||||
"menu.newCampaign": "Nouvelle campagne",
|
"menu.newCampaign": "Nouvelle campagne",
|
||||||
"menu.settings": "Paramètres",
|
"menu.settings": "Paramètres",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "La liste de diffusion est introuvable.",
|
"public.campaignNotFound": "La liste de diffusion est introuvable.",
|
||||||
"public.confirmOptinSubTitle": "Confirmer votre abonnement",
|
"public.confirmOptinSubTitle": "Confirmer votre abonnement",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Ismeretlen szolgáltatás.",
|
"bounces.unknownService": "Ismeretlen szolgáltatás.",
|
||||||
"bounces.view": "Visszapattanások megtekintése",
|
"bounces.view": "Visszapattanások megtekintése",
|
||||||
"campaigns.addAltText": "Alternatív egyszerű szöveges üzenet hozzáadása",
|
"campaigns.addAltText": "Alternatív egyszerű szöveges üzenet hozzáadása",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Nem lehet frissíteni a futó vagy a befejezett kampányt.",
|
"campaigns.cantUpdate": "Nem lehet frissíteni a futó vagy a befejezett kampányt.",
|
||||||
"campaigns.clicks": "Kattintások",
|
"campaigns.clicks": "Kattintások",
|
||||||
"campaigns.confirmDelete": "Törlés {name}",
|
"campaigns.confirmDelete": "Törlés {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Média",
|
"menu.media": "Média",
|
||||||
"menu.newCampaign": "Új készítése",
|
"menu.newCampaign": "Új készítése",
|
||||||
"menu.settings": "Beállítások",
|
"menu.settings": "Beállítások",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Az e-mail üzenet nem található.",
|
"public.campaignNotFound": "Az e-mail üzenet nem található.",
|
||||||
"public.confirmOptinSubTitle": "Feliratkozás megerősítése",
|
"public.confirmOptinSubTitle": "Feliratkozás megerősítése",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Servizio sconosciuto.",
|
"bounces.unknownService": "Servizio sconosciuto.",
|
||||||
"bounces.view": "Visualizza i rimbalzi",
|
"bounces.view": "Visualizza i rimbalzi",
|
||||||
"campaigns.addAltText": "Aggiungere un messaggio sostitutivo in testo semplice",
|
"campaigns.addAltText": "Aggiungere un messaggio sostitutivo in testo semplice",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Impossibile aggiornare una campagna in corso o già effettuata.",
|
"campaigns.cantUpdate": "Impossibile aggiornare una campagna in corso o già effettuata.",
|
||||||
"campaigns.clicks": "Clic",
|
"campaigns.clicks": "Clic",
|
||||||
"campaigns.confirmDelete": "Cancellare {nome}",
|
"campaigns.confirmDelete": "Cancellare {nome}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Media",
|
"menu.media": "Media",
|
||||||
"menu.newCampaign": "Creare nuovo",
|
"menu.newCampaign": "Creare nuovo",
|
||||||
"menu.settings": "Impostazioni",
|
"menu.settings": "Impostazioni",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Newsletter impossibile da trovare.",
|
"public.campaignNotFound": "Newsletter impossibile da trovare.",
|
||||||
"public.confirmOptinSubTitle": "Confermare l'iscrizione",
|
"public.confirmOptinSubTitle": "Confermare l'iscrizione",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "不明のサービス。",
|
"bounces.unknownService": "不明のサービス。",
|
||||||
"bounces.view": "バウンスビュー",
|
"bounces.view": "バウンスビュー",
|
||||||
"campaigns.addAltText": "代替のプレーンテキストメッセージを追加する",
|
"campaigns.addAltText": "代替のプレーンテキストメッセージを追加する",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "実行中又は終了しているキャンペーンの更新はできません。",
|
"campaigns.cantUpdate": "実行中又は終了しているキャンペーンの更新はできません。",
|
||||||
"campaigns.clicks": "クリック",
|
"campaigns.clicks": "クリック",
|
||||||
"campaigns.confirmDelete": "削除 {name}",
|
"campaigns.confirmDelete": "削除 {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "メディア",
|
"menu.media": "メディア",
|
||||||
"menu.newCampaign": "新規作成",
|
"menu.newCampaign": "新規作成",
|
||||||
"menu.settings": "設定",
|
"menu.settings": "設定",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "メールのメッセージが見つかりませんでした。",
|
"public.campaignNotFound": "メールのメッセージが見つかりませんでした。",
|
||||||
"public.confirmOptinSubTitle": "サブスクリプション確認",
|
"public.confirmOptinSubTitle": "サブスクリプション確認",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Unknown service.",
|
"bounces.unknownService": "Unknown service.",
|
||||||
"bounces.view": "View bounces",
|
"bounces.view": "View bounces",
|
||||||
"campaigns.addAltText": "Add alternate plain text message",
|
"campaigns.addAltText": "Add alternate plain text message",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "ഇപ്പോൾ നടന്നുകൊണ്ടിരിയ്ക്കുന്നതോ, അവസാനിച്ചതോ ആയ ക്യാമ്പേയ്ൻ പുതുക്കാനാകില്ല.",
|
"campaigns.cantUpdate": "ഇപ്പോൾ നടന്നുകൊണ്ടിരിയ്ക്കുന്നതോ, അവസാനിച്ചതോ ആയ ക്യാമ്പേയ്ൻ പുതുക്കാനാകില്ല.",
|
||||||
"campaigns.clicks": "ക്ലീക്കുകൾ",
|
"campaigns.clicks": "ക്ലീക്കുകൾ",
|
||||||
"campaigns.confirmDelete": "{name} നീക്കം ചെയ്യുക",
|
"campaigns.confirmDelete": "{name} നീക്കം ചെയ്യുക",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "മീഡിയ",
|
"menu.media": "മീഡിയ",
|
||||||
"menu.newCampaign": "പുതിയത് തുടങ്ങുക",
|
"menu.newCampaign": "പുതിയത് തുടങ്ങുക",
|
||||||
"menu.settings": "ക്രമീകരണങ്ങൾ",
|
"menu.settings": "ക്രമീകരണങ്ങൾ",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "ഇ-മെയിൽ കണ്ടെത്താനായില്ല.",
|
"public.campaignNotFound": "ഇ-മെയിൽ കണ്ടെത്താനായില്ല.",
|
||||||
"public.confirmOptinSubTitle": "വരിക്കാരനാകുന്നത് സ്ഥിരീകരിക്കുക",
|
"public.confirmOptinSubTitle": "വരിക്കാരനാകുന്നത് സ്ഥിരീകരിക്കുക",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Onbekende service.",
|
"bounces.unknownService": "Onbekende service.",
|
||||||
"bounces.view": "Zie bounces",
|
"bounces.view": "Zie bounces",
|
||||||
"campaigns.addAltText": "Voeg alternatieve tekst zonder opmaak toe",
|
"campaigns.addAltText": "Voeg alternatieve tekst zonder opmaak toe",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Kan een lopende of afgelopen campagne niet updaten.",
|
"campaigns.cantUpdate": "Kan een lopende of afgelopen campagne niet updaten.",
|
||||||
"campaigns.clicks": "Kliks",
|
"campaigns.clicks": "Kliks",
|
||||||
"campaigns.confirmDelete": "Verwijder {name}",
|
"campaigns.confirmDelete": "Verwijder {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Media",
|
"menu.media": "Media",
|
||||||
"menu.newCampaign": "Nieuwe aanmaken",
|
"menu.newCampaign": "Nieuwe aanmaken",
|
||||||
"menu.settings": "Instellingen",
|
"menu.settings": "Instellingen",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Het e-mailbericht werd niet gevonden.",
|
"public.campaignNotFound": "Het e-mailbericht werd niet gevonden.",
|
||||||
"public.confirmOptinSubTitle": "Bevestig inschrijving",
|
"public.confirmOptinSubTitle": "Bevestig inschrijving",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Nieznane usługi.",
|
"bounces.unknownService": "Nieznane usługi.",
|
||||||
"bounces.view": "Zobacz odbicia",
|
"bounces.view": "Zobacz odbicia",
|
||||||
"campaigns.addAltText": "Dodaj alternatywną wiadomość jako plain text",
|
"campaigns.addAltText": "Dodaj alternatywną wiadomość jako plain text",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Nie można aktualizować aktywnej ani zakończonej kampanii",
|
"campaigns.cantUpdate": "Nie można aktualizować aktywnej ani zakończonej kampanii",
|
||||||
"campaigns.clicks": "Kliknięcia",
|
"campaigns.clicks": "Kliknięcia",
|
||||||
"campaigns.confirmDelete": "Usuń {name}",
|
"campaigns.confirmDelete": "Usuń {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Media",
|
"menu.media": "Media",
|
||||||
"menu.newCampaign": "Utwórz nową",
|
"menu.newCampaign": "Utwórz nową",
|
||||||
"menu.settings": "Ustawienia",
|
"menu.settings": "Ustawienia",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Wiadomość email nie została znaleziona.",
|
"public.campaignNotFound": "Wiadomość email nie została znaleziona.",
|
||||||
"public.confirmOptinSubTitle": "Potwierdź subskrypcję",
|
"public.confirmOptinSubTitle": "Potwierdź subskrypcję",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Serviço desconhecido.",
|
"bounces.unknownService": "Serviço desconhecido.",
|
||||||
"bounces.view": "Ver bounces",
|
"bounces.view": "Ver bounces",
|
||||||
"campaigns.addAltText": "Adicionar mensagem alternativa em texto simples",
|
"campaigns.addAltText": "Adicionar mensagem alternativa em texto simples",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Não é possível atualizar uma campanha em execução ou finalizada.",
|
"campaigns.cantUpdate": "Não é possível atualizar uma campanha em execução ou finalizada.",
|
||||||
"campaigns.clicks": "Cliques",
|
"campaigns.clicks": "Cliques",
|
||||||
"campaigns.confirmDelete": "Excluir {name}",
|
"campaigns.confirmDelete": "Excluir {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Mídia",
|
"menu.media": "Mídia",
|
||||||
"menu.newCampaign": "Criar nova",
|
"menu.newCampaign": "Criar nova",
|
||||||
"menu.settings": "Configurações",
|
"menu.settings": "Configurações",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "A mensagem do e-mail não foi encontrada.",
|
"public.campaignNotFound": "A mensagem do e-mail não foi encontrada.",
|
||||||
"public.confirmOptinSubTitle": "Confirmar a assinatura",
|
"public.confirmOptinSubTitle": "Confirmar a assinatura",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Unknown service.",
|
"bounces.unknownService": "Unknown service.",
|
||||||
"bounces.view": "View bounces",
|
"bounces.view": "View bounces",
|
||||||
"campaigns.addAltText": "Adicionar mensagem alternativa em texto simples",
|
"campaigns.addAltText": "Adicionar mensagem alternativa em texto simples",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Não é possível atualizar uma campanha em curso ou terminada.",
|
"campaigns.cantUpdate": "Não é possível atualizar uma campanha em curso ou terminada.",
|
||||||
"campaigns.clicks": "Cliques",
|
"campaigns.clicks": "Cliques",
|
||||||
"campaigns.confirmDelete": "Eliminar {name}",
|
"campaigns.confirmDelete": "Eliminar {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Mídia",
|
"menu.media": "Mídia",
|
||||||
"menu.newCampaign": "Criar nova",
|
"menu.newCampaign": "Criar nova",
|
||||||
"menu.settings": "Definições",
|
"menu.settings": "Definições",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "A mensagem de email não foi encontrada.",
|
"public.campaignNotFound": "A mensagem de email não foi encontrada.",
|
||||||
"public.confirmOptinSubTitle": "Confirmar subscrição",
|
"public.confirmOptinSubTitle": "Confirmar subscrição",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Serviciu necunoscut.",
|
"bounces.unknownService": "Serviciu necunoscut.",
|
||||||
"bounces.view": "Vizualizeaz[ respingeri",
|
"bounces.view": "Vizualizeaz[ respingeri",
|
||||||
"campaigns.addAltText": "Adaug[ un text simplu alternativ",
|
"campaigns.addAltText": "Adaug[ un text simplu alternativ",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Nu se poate actualiza o campaniedifuzată sau terminată",
|
"campaigns.cantUpdate": "Nu se poate actualiza o campaniedifuzată sau terminată",
|
||||||
"campaigns.clicks": "Clickuri",
|
"campaigns.clicks": "Clickuri",
|
||||||
"campaigns.confirmDelete": "Sterge {nume}",
|
"campaigns.confirmDelete": "Sterge {nume}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Media",
|
"menu.media": "Media",
|
||||||
"menu.newCampaign": "Creaza nou",
|
"menu.newCampaign": "Creaza nou",
|
||||||
"menu.settings": "Setări",
|
"menu.settings": "Setări",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Mesajul emailului nu a fost găsit.",
|
"public.campaignNotFound": "Mesajul emailului nu a fost găsit.",
|
||||||
"public.confirmOptinSubTitle": "Confirmă abonarea",
|
"public.confirmOptinSubTitle": "Confirmă abonarea",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Неизвестная услуга.",
|
"bounces.unknownService": "Неизвестная услуга.",
|
||||||
"bounces.view": "Просмотр отскоков",
|
"bounces.view": "Просмотр отскоков",
|
||||||
"campaigns.addAltText": "Добавить альтернативное простое текстовое сообщение",
|
"campaigns.addAltText": "Добавить альтернативное простое текстовое сообщение",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Не возможно обновить запущенную или завершённую компанию.",
|
"campaigns.cantUpdate": "Не возможно обновить запущенную или завершённую компанию.",
|
||||||
"campaigns.clicks": "Клики",
|
"campaigns.clicks": "Клики",
|
||||||
"campaigns.confirmDelete": "Удалить {name}",
|
"campaigns.confirmDelete": "Удалить {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Медиа",
|
"menu.media": "Медиа",
|
||||||
"menu.newCampaign": "Создать новую",
|
"menu.newCampaign": "Создать новую",
|
||||||
"menu.settings": "Параметры",
|
"menu.settings": "Параметры",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Письмо не было найдено.",
|
"public.campaignNotFound": "Письмо не было найдено.",
|
||||||
"public.confirmOptinSubTitle": "Подтверждение подписки",
|
"public.confirmOptinSubTitle": "Подтверждение подписки",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Unknown service.",
|
"bounces.unknownService": "Unknown service.",
|
||||||
"bounces.view": "View bounces",
|
"bounces.view": "View bounces",
|
||||||
"campaigns.addAltText": "Alternatif düz metin ekleyin",
|
"campaigns.addAltText": "Alternatif düz metin ekleyin",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Gönderilmekte olan veya gönderilmiş kampaynalar güncellenemez.",
|
"campaigns.cantUpdate": "Gönderilmekte olan veya gönderilmiş kampaynalar güncellenemez.",
|
||||||
"campaigns.clicks": "Tıklama",
|
"campaigns.clicks": "Tıklama",
|
||||||
"campaigns.confirmDelete": "Sil {name}",
|
"campaigns.confirmDelete": "Sil {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Medya",
|
"menu.media": "Medya",
|
||||||
"menu.newCampaign": "Yeni oluştur",
|
"menu.newCampaign": "Yeni oluştur",
|
||||||
"menu.settings": "Ayarlar",
|
"menu.settings": "Ayarlar",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "E-posta mesajı bulunamadı.",
|
"public.campaignNotFound": "E-posta mesajı bulunamadı.",
|
||||||
"public.confirmOptinSubTitle": "Üyeliği doğrula",
|
"public.confirmOptinSubTitle": "Üyeliği doğrula",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "Dịch vụ không xác định.",
|
"bounces.unknownService": "Dịch vụ không xác định.",
|
||||||
"bounces.view": "Xem thư bị trả lại",
|
"bounces.view": "Xem thư bị trả lại",
|
||||||
"campaigns.addAltText": "Thêm tin nhắn văn bản thuần túy thay thế",
|
"campaigns.addAltText": "Thêm tin nhắn văn bản thuần túy thay thế",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "Không thể cập nhật chiến dịch đang chạy hoặc đã kết thúc.",
|
"campaigns.cantUpdate": "Không thể cập nhật chiến dịch đang chạy hoặc đã kết thúc.",
|
||||||
"campaigns.clicks": "Số lần nhấp chuột",
|
"campaigns.clicks": "Số lần nhấp chuột",
|
||||||
"campaigns.confirmDelete": "Xóa {name}",
|
"campaigns.confirmDelete": "Xóa {name}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "Dữ liệu truyền thông",
|
"menu.media": "Dữ liệu truyền thông",
|
||||||
"menu.newCampaign": "Tạo mới",
|
"menu.newCampaign": "Tạo mới",
|
||||||
"menu.settings": "Cài đặt",
|
"menu.settings": "Cài đặt",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "Tin nhắn e-mail không được tìm thấy.",
|
"public.campaignNotFound": "Tin nhắn e-mail không được tìm thấy.",
|
||||||
"public.confirmOptinSubTitle": "Xác nhận đăng ký",
|
"public.confirmOptinSubTitle": "Xác nhận đăng ký",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "未知的服务。",
|
"bounces.unknownService": "未知的服务。",
|
||||||
"bounces.view": "查看退回邮",
|
"bounces.view": "查看退回邮",
|
||||||
"campaigns.addAltText": "添加备用纯文本消息",
|
"campaigns.addAltText": "添加备用纯文本消息",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "无法更新正在运行或已完成的广告系列。",
|
"campaigns.cantUpdate": "无法更新正在运行或已完成的广告系列。",
|
||||||
"campaigns.clicks": "点击次数",
|
"campaigns.clicks": "点击次数",
|
||||||
"campaigns.confirmDelete": "删除{名称}",
|
"campaigns.confirmDelete": "删除{名称}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "媒体",
|
"menu.media": "媒体",
|
||||||
"menu.newCampaign": "创建新的",
|
"menu.newCampaign": "创建新的",
|
||||||
"menu.settings": "设置",
|
"menu.settings": "设置",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "未找到电子邮件。",
|
"public.campaignNotFound": "未找到电子邮件。",
|
||||||
"public.confirmOptinSubTitle": "确认订阅",
|
"public.confirmOptinSubTitle": "确认订阅",
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
"bounces.unknownService": "未知的服務。",
|
"bounces.unknownService": "未知的服務。",
|
||||||
"bounces.view": "查看退回郵",
|
"bounces.view": "查看退回郵",
|
||||||
"campaigns.addAltText": "添加備用純文本消息",
|
"campaigns.addAltText": "添加備用純文本消息",
|
||||||
|
"campaigns.archive": "Archive",
|
||||||
|
"campaigns.archiveEnable": "Publish to public archive",
|
||||||
|
"campaigns.archiveHelp": "Publish (running, paused, finished) the campaign message on the public archive.",
|
||||||
|
"campaigns.archiveMeta": "Campaign metadata",
|
||||||
|
"campaigns.archiveMetaHelp": "Dummy subscriber data to use in the public message including name, email, and any optional attributes used in the campaign message or template.",
|
||||||
"campaigns.cantUpdate": "無法更新正在運行或已完成的廣告系列。",
|
"campaigns.cantUpdate": "無法更新正在運行或已完成的廣告系列。",
|
||||||
"campaigns.clicks": "點擊次數",
|
"campaigns.clicks": "點擊次數",
|
||||||
"campaigns.confirmDelete": "刪除{名稱}",
|
"campaigns.confirmDelete": "刪除{名稱}",
|
||||||
|
@ -282,6 +287,8 @@
|
||||||
"menu.media": "媒體",
|
"menu.media": "媒體",
|
||||||
"menu.newCampaign": "創建新的",
|
"menu.newCampaign": "創建新的",
|
||||||
"menu.settings": "設置",
|
"menu.settings": "設置",
|
||||||
|
"public.archiveEmpty": "No archived messages yet.",
|
||||||
|
"public.archiveTitle": "Mailing list archive",
|
||||||
"public.blocklisted": "Permanently unsubscribed.",
|
"public.blocklisted": "Permanently unsubscribed.",
|
||||||
"public.campaignNotFound": "未找到電子郵件。",
|
"public.campaignNotFound": "未找到電子郵件。",
|
||||||
"public.confirmOptinSubTitle": "確認訂閱",
|
"public.confirmOptinSubTitle": "確認訂閱",
|
||||||
|
|
|
@ -16,6 +16,9 @@ const (
|
||||||
CampaignAnalyticsViews = "views"
|
CampaignAnalyticsViews = "views"
|
||||||
CampaignAnalyticsClicks = "clicks"
|
CampaignAnalyticsClicks = "clicks"
|
||||||
CampaignAnalyticsBounces = "bounces"
|
CampaignAnalyticsBounces = "bounces"
|
||||||
|
|
||||||
|
campaignTplDefault = "default"
|
||||||
|
campaignTplArchive = "archive"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QueryCampaigns retrieves paginated campaigns optionally filtering them by the given arbitrary
|
// QueryCampaigns retrieves paginated campaigns optionally filtering them by the given arbitrary
|
||||||
|
@ -59,6 +62,28 @@ func (c *Core) QueryCampaigns(searchStr string, statuses []string, orderBy, orde
|
||||||
|
|
||||||
// GetCampaign retrieves a campaign.
|
// GetCampaign retrieves a campaign.
|
||||||
func (c *Core) GetCampaign(id int, uuid string) (models.Campaign, error) {
|
func (c *Core) GetCampaign(id int, uuid string) (models.Campaign, error) {
|
||||||
|
return c.getCampaign(id, uuid, campaignTplDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArchivedCampaign retreives a campaign with the archive template body.
|
||||||
|
func (c *Core) GetArchivedCampaign(id int, uuid string) (models.Campaign, error) {
|
||||||
|
out, err := c.getCampaign(id, uuid, campaignTplArchive)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !out.Archive {
|
||||||
|
return models.Campaign{}, echo.NewHTTPError(http.StatusBadRequest,
|
||||||
|
c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCampaign retrieves a campaign. If typlType=default, then the campaign's
|
||||||
|
// template body is returned as "template_body". If tplType="archive",
|
||||||
|
// the archive template is returned.
|
||||||
|
func (c *Core) getCampaign(id int, uuid string, tplType string) (models.Campaign, error) {
|
||||||
// Unsafe to ignore scanning fields not present in models.Campaigns.
|
// Unsafe to ignore scanning fields not present in models.Campaigns.
|
||||||
var uu interface{}
|
var uu interface{}
|
||||||
if uuid != "" {
|
if uuid != "" {
|
||||||
|
@ -66,7 +91,7 @@ func (c *Core) GetCampaign(id int, uuid string) (models.Campaign, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var out models.Campaigns
|
var out models.Campaigns
|
||||||
if err := c.q.GetCampaign.Select(&out, id, uu); err != nil {
|
if err := c.q.GetCampaign.Select(&out, id, uu, tplType); err != nil {
|
||||||
// if err := c.db.Select(&out, stmt, 0, pq.Array([]string{}), queryStr, 0, 1); err != nil {
|
// if err := c.db.Select(&out, stmt, 0, pq.Array([]string{}), queryStr, 0, 1); err != nil {
|
||||||
c.log.Printf("error fetching campaign: %v", err)
|
c.log.Printf("error fetching campaign: %v", err)
|
||||||
return models.Campaign{}, echo.NewHTTPError(http.StatusInternalServerError,
|
return models.Campaign{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
@ -76,7 +101,6 @@ func (c *Core) GetCampaign(id int, uuid string) (models.Campaign, error) {
|
||||||
if len(out) == 0 {
|
if len(out) == 0 {
|
||||||
return models.Campaign{}, echo.NewHTTPError(http.StatusBadRequest,
|
return models.Campaign{}, echo.NewHTTPError(http.StatusBadRequest,
|
||||||
c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
|
c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(out); i++ {
|
for i := 0; i < len(out); i++ {
|
||||||
|
@ -113,6 +137,23 @@ func (c *Core) GetCampaignForPreview(id, tplID int) (models.Campaign, error) {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetArchivedCampaigns retrieves campaigns with a template body.
|
||||||
|
func (c *Core) GetArchivedCampaigns(offset, limit int) (models.Campaigns, int, error) {
|
||||||
|
var out models.Campaigns
|
||||||
|
if err := c.q.GetArchivedCampaigns.Select(&out, offset, limit); err != nil {
|
||||||
|
c.log.Printf("error fetching public campaigns: %v", err)
|
||||||
|
return models.Campaigns{}, 0, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(out) > 0 {
|
||||||
|
total = out[0].Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCampaign creates a new campaign.
|
// CreateCampaign creates a new campaign.
|
||||||
func (c *Core) CreateCampaign(o models.Campaign, listIDs []int) (models.Campaign, error) {
|
func (c *Core) CreateCampaign(o models.Campaign, listIDs []int) (models.Campaign, error) {
|
||||||
uu, err := uuid.NewV4()
|
uu, err := uuid.NewV4()
|
||||||
|
@ -139,6 +180,9 @@ func (c *Core) CreateCampaign(o models.Campaign, listIDs []int) (models.Campaign
|
||||||
o.Messenger,
|
o.Messenger,
|
||||||
o.TemplateID,
|
o.TemplateID,
|
||||||
pq.Array(listIDs),
|
pq.Array(listIDs),
|
||||||
|
o.Archive,
|
||||||
|
o.ArchiveTemplateID,
|
||||||
|
o.ArchiveMeta,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return models.Campaign{}, echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("campaigns.noSubs"))
|
return models.Campaign{}, echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("campaigns.noSubs"))
|
||||||
|
@ -172,7 +216,10 @@ func (c *Core) UpdateCampaign(id int, o models.Campaign, listIDs []int, sendLate
|
||||||
pq.StringArray(normalizeTags(o.Tags)),
|
pq.StringArray(normalizeTags(o.Tags)),
|
||||||
o.Messenger,
|
o.Messenger,
|
||||||
o.TemplateID,
|
o.TemplateID,
|
||||||
pq.Array(listIDs))
|
pq.Array(listIDs),
|
||||||
|
o.Archive,
|
||||||
|
o.ArchiveTemplateID,
|
||||||
|
o.ArchiveMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.Printf("error updating campaign: %v", err)
|
c.log.Printf("error updating campaign: %v", err)
|
||||||
return models.Campaign{}, echo.NewHTTPError(http.StatusInternalServerError,
|
return models.Campaign{}, echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
@ -243,6 +290,18 @@ func (c *Core) UpdateCampaignStatus(id int, status string) (models.Campaign, err
|
||||||
return cm, nil
|
return cm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateCampaignArchive updates a campaign's archive properties.
|
||||||
|
func (c *Core) UpdateCampaignArchive(id int, enabled bool, tplID int, meta models.JSON) error {
|
||||||
|
if _, err := c.q.UpdateCampaignArchive.Exec(id, enabled, tplID, meta); err != nil {
|
||||||
|
c.log.Printf("error updating campaign: %v", err)
|
||||||
|
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
|
c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteCampaign deletes a campaign.
|
// DeleteCampaign deletes a campaign.
|
||||||
func (c *Core) DeleteCampaign(id int) error {
|
func (c *Core) DeleteCampaign(id int) error {
|
||||||
res, err := c.q.DeleteCampaign.Exec(id)
|
res, err := c.q.DeleteCampaign.Exec(id)
|
||||||
|
|
|
@ -125,6 +125,7 @@ type Config struct {
|
||||||
OptinURL string
|
OptinURL string
|
||||||
MessageURL string
|
MessageURL string
|
||||||
ViewTrackURL string
|
ViewTrackURL string
|
||||||
|
ArchiveURL string
|
||||||
UnsubHeader bool
|
UnsubHeader bool
|
||||||
|
|
||||||
// Interval to scan the DB for active campaign checkpoints.
|
// Interval to scan the DB for active campaign checkpoints.
|
||||||
|
@ -462,6 +463,9 @@ func (m *Manager) TemplateFuncs(c *models.Campaign) template.FuncMap {
|
||||||
"MessageURL": func(msg *CampaignMessage) string {
|
"MessageURL": func(msg *CampaignMessage) string {
|
||||||
return fmt.Sprintf(m.cfg.MessageURL, c.UUID, msg.Subscriber.UUID)
|
return fmt.Sprintf(m.cfg.MessageURL, c.UUID, msg.Subscriber.UUID)
|
||||||
},
|
},
|
||||||
|
"ArchiveURL": func() string {
|
||||||
|
return m.cfg.ArchiveURL
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range m.tplFuncs {
|
for k, v := range m.tplFuncs {
|
||||||
|
|
|
@ -17,6 +17,18 @@ func V2_3_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add archive publishing field to campaigns.
|
||||||
|
if _, err := db.Exec(`ALTER TABLE campaigns
|
||||||
|
ADD COLUMN IF NOT EXISTS archive BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN IF NOT EXISTS archive_meta JSONB NOT NULL DEFAULT '{}',
|
||||||
|
ADD COLUMN IF NOT EXISTS archive_template_id INTEGER REFERENCES templates(id) ON DELETE SET DEFAULT DEFAULT 1
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if _, err := db.Exec(`ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS "publish_meta" JSONB NOT NULL DEFAULT '{}'`); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
// Insert new preference settings.
|
// Insert new preference settings.
|
||||||
if _, err := db.Exec(`
|
if _, err := db.Exec(`
|
||||||
INSERT INTO settings (key, value) VALUES
|
INSERT INTO settings (key, value) VALUES
|
||||||
|
|
|
@ -234,26 +234,30 @@ type Campaign struct {
|
||||||
Base
|
Base
|
||||||
CampaignMeta
|
CampaignMeta
|
||||||
|
|
||||||
UUID string `db:"uuid" json:"uuid"`
|
UUID string `db:"uuid" json:"uuid"`
|
||||||
Type string `db:"type" json:"type"`
|
Type string `db:"type" json:"type"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Subject string `db:"subject" json:"subject"`
|
Subject string `db:"subject" json:"subject"`
|
||||||
FromEmail string `db:"from_email" json:"from_email"`
|
FromEmail string `db:"from_email" json:"from_email"`
|
||||||
Body string `db:"body" json:"body"`
|
Body string `db:"body" json:"body"`
|
||||||
AltBody null.String `db:"altbody" json:"altbody"`
|
AltBody null.String `db:"altbody" json:"altbody"`
|
||||||
SendAt null.Time `db:"send_at" json:"send_at"`
|
SendAt null.Time `db:"send_at" json:"send_at"`
|
||||||
Status string `db:"status" json:"status"`
|
Status string `db:"status" json:"status"`
|
||||||
ContentType string `db:"content_type" json:"content_type"`
|
ContentType string `db:"content_type" json:"content_type"`
|
||||||
Tags pq.StringArray `db:"tags" json:"tags"`
|
Tags pq.StringArray `db:"tags" json:"tags"`
|
||||||
Headers Headers `db:"headers" json:"headers"`
|
Headers Headers `db:"headers" json:"headers"`
|
||||||
TemplateID int `db:"template_id" json:"template_id"`
|
TemplateID int `db:"template_id" json:"template_id"`
|
||||||
Messenger string `db:"messenger" json:"messenger"`
|
Messenger string `db:"messenger" json:"messenger"`
|
||||||
|
Archive bool `db:"archive" json:"archive"`
|
||||||
|
ArchiveTemplateID int `db:"archive_template_id" json:"archive_template_id"`
|
||||||
|
ArchiveMeta json.RawMessage `db:"archive_meta" json:"archive_meta"`
|
||||||
|
|
||||||
// TemplateBody is joined in from templates by the next-campaigns query.
|
// TemplateBody is joined in from templates by the next-campaigns query.
|
||||||
TemplateBody string `db:"template_body" json:"-"`
|
TemplateBody string `db:"template_body" json:"-"`
|
||||||
Tpl *template.Template `json:"-"`
|
ArchiveTemplateBody string `db:"archive_template_body" json:"-"`
|
||||||
SubjectTpl *txttpl.Template `json:"-"`
|
Tpl *template.Template `json:"-"`
|
||||||
AltBodyTpl *template.Template `json:"-"`
|
SubjectTpl *txttpl.Template `json:"-"`
|
||||||
|
AltBodyTpl *template.Template `json:"-"`
|
||||||
|
|
||||||
// Pseudofield for getting the total number of subscribers
|
// Pseudofield for getting the total number of subscribers
|
||||||
// in searches and queries.
|
// in searches and queries.
|
||||||
|
@ -474,6 +478,26 @@ func (camps Campaigns) LoadStats(stmt *sqlx.Stmt) error {
|
||||||
// CompileTemplate compiles a campaign body template into its base
|
// CompileTemplate compiles a campaign body template into its base
|
||||||
// template and sets the resultant template to Campaign.Tpl.
|
// template and sets the resultant template to Campaign.Tpl.
|
||||||
func (c *Campaign) CompileTemplate(f template.FuncMap) error {
|
func (c *Campaign) CompileTemplate(f template.FuncMap) error {
|
||||||
|
// If the subject line has a template string, compile it.
|
||||||
|
if strings.Contains(c.Subject, "{{") {
|
||||||
|
subj := c.Subject
|
||||||
|
for _, r := range regTplFuncs {
|
||||||
|
subj = r.regExp.ReplaceAllString(subj, r.replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
var txtFuncs map[string]interface{} = f
|
||||||
|
subjTpl, err := txttpl.New(ContentTpl).Funcs(txtFuncs).Parse(subj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error compiling subject: %v", err)
|
||||||
|
}
|
||||||
|
c.SubjectTpl = subjTpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// No template or body. Nothing to compile.
|
||||||
|
if c.TemplateBody == "" || c.Body == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Compile the base template.
|
// Compile the base template.
|
||||||
body := c.TemplateBody
|
body := c.TemplateBody
|
||||||
for _, r := range regTplFuncs {
|
for _, r := range regTplFuncs {
|
||||||
|
@ -511,21 +535,6 @@ func (c *Campaign) CompileTemplate(f template.FuncMap) error {
|
||||||
}
|
}
|
||||||
c.Tpl = out
|
c.Tpl = out
|
||||||
|
|
||||||
// If the subject line has a template string, compile it.
|
|
||||||
if strings.Contains(c.Subject, "{{") {
|
|
||||||
subj := c.Subject
|
|
||||||
for _, r := range regTplFuncs {
|
|
||||||
subj = r.regExp.ReplaceAllString(subj, r.replace)
|
|
||||||
}
|
|
||||||
|
|
||||||
var txtFuncs map[string]interface{} = f
|
|
||||||
subjTpl, err := txttpl.New(ContentTpl).Funcs(txtFuncs).Parse(subj)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error compiling subject: %v", err)
|
|
||||||
}
|
|
||||||
c.SubjectTpl = subjTpl
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(c.AltBody.String, "{{") {
|
if strings.Contains(c.AltBody.String, "{{") {
|
||||||
b := c.AltBody.String
|
b := c.AltBody.String
|
||||||
for _, r := range regTplFuncs {
|
for _, r := range regTplFuncs {
|
||||||
|
|
|
@ -61,6 +61,7 @@ type Queries struct {
|
||||||
GetCampaignForPreview *sqlx.Stmt `query:"get-campaign-for-preview"`
|
GetCampaignForPreview *sqlx.Stmt `query:"get-campaign-for-preview"`
|
||||||
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
||||||
GetCampaignStatus *sqlx.Stmt `query:"get-campaign-status"`
|
GetCampaignStatus *sqlx.Stmt `query:"get-campaign-status"`
|
||||||
|
GetArchivedCampaigns *sqlx.Stmt `query:"get-archived-campaigns"`
|
||||||
|
|
||||||
// These two queries are read as strings and based on settings.individual_tracking=on/off,
|
// These two queries are read as strings and based on settings.individual_tracking=on/off,
|
||||||
// are interpolated and copied to view and click counts. Same query, different tables.
|
// are interpolated and copied to view and click counts. Same query, different tables.
|
||||||
|
@ -79,6 +80,7 @@ type Queries struct {
|
||||||
UpdateCampaign *sqlx.Stmt `query:"update-campaign"`
|
UpdateCampaign *sqlx.Stmt `query:"update-campaign"`
|
||||||
UpdateCampaignStatus *sqlx.Stmt `query:"update-campaign-status"`
|
UpdateCampaignStatus *sqlx.Stmt `query:"update-campaign-status"`
|
||||||
UpdateCampaignCounts *sqlx.Stmt `query:"update-campaign-counts"`
|
UpdateCampaignCounts *sqlx.Stmt `query:"update-campaign-counts"`
|
||||||
|
UpdateCampaignArchive *sqlx.Stmt `query:"update-campaign-archive"`
|
||||||
RegisterCampaignView *sqlx.Stmt `query:"register-campaign-view"`
|
RegisterCampaignView *sqlx.Stmt `query:"register-campaign-view"`
|
||||||
DeleteCampaign *sqlx.Stmt `query:"delete-campaign"`
|
DeleteCampaign *sqlx.Stmt `query:"delete-campaign"`
|
||||||
|
|
||||||
|
|
28
queries.sql
28
queries.sql
|
@ -473,8 +473,8 @@ counts AS (
|
||||||
AND subscribers.status='enabled'
|
AND subscribers.status='enabled'
|
||||||
),
|
),
|
||||||
camp AS (
|
camp AS (
|
||||||
INSERT INTO campaigns (uuid, type, name, subject, from_email, body, altbody, content_type, send_at, headers, tags, messenger, template_id, to_send, max_subscriber_id)
|
INSERT INTO campaigns (uuid, type, name, subject, from_email, body, altbody, content_type, send_at, headers, tags, messenger, template_id, to_send, max_subscriber_id, archive, archive_template_id, archive_meta)
|
||||||
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, (SELECT id FROM tpl), (SELECT to_send FROM counts), (SELECT max_sub_id FROM counts)
|
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, (SELECT id FROM tpl), (SELECT to_send FROM counts), (SELECT max_sub_id FROM counts), $15, $16, $17
|
||||||
RETURNING id
|
RETURNING id
|
||||||
)
|
)
|
||||||
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
||||||
|
@ -491,7 +491,7 @@ INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
||||||
SELECT c.id, c.uuid, c.name, c.subject, c.from_email,
|
SELECT c.id, c.uuid, c.name, c.subject, c.from_email,
|
||||||
c.messenger, c.started_at, c.to_send, c.sent, c.type,
|
c.messenger, c.started_at, c.to_send, c.sent, c.type,
|
||||||
c.body, c.altbody, c.send_at, c.headers, c.status, c.content_type, c.tags,
|
c.body, c.altbody, c.send_at, c.headers, c.status, c.content_type, c.tags,
|
||||||
c.template_id, c.created_at, c.updated_at,
|
c.template_id, c.archive, c.archive_template_id, c.archive_meta, c.created_at, c.updated_at,
|
||||||
COUNT(*) OVER () AS total,
|
COUNT(*) OVER () AS total,
|
||||||
(
|
(
|
||||||
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(l)), '[]') FROM (
|
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(l)), '[]') FROM (
|
||||||
|
@ -510,9 +510,17 @@ ORDER BY %s %s OFFSET $4 LIMIT (CASE WHEN $5 = 0 THEN NULL ELSE $5 END);
|
||||||
SELECT campaigns.*,
|
SELECT campaigns.*,
|
||||||
COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body
|
COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body
|
||||||
FROM campaigns
|
FROM campaigns
|
||||||
LEFT JOIN templates ON (templates.id = campaigns.template_id)
|
LEFT JOIN templates ON (
|
||||||
|
CASE WHEN $3 = 'default' THEN templates.id = campaigns.template_id
|
||||||
|
ELSE templates.id = campaigns.archive_template_id END
|
||||||
|
)
|
||||||
WHERE CASE WHEN $1 > 0 THEN campaigns.id = $1 ELSE uuid = $2 END;
|
WHERE CASE WHEN $1 > 0 THEN campaigns.id = $1 ELSE uuid = $2 END;
|
||||||
|
|
||||||
|
-- name: get-archived-campaigns
|
||||||
|
SELECT COUNT(*) OVER () AS total, id, uuid, subject, archive_meta, created_at FROM campaigns
|
||||||
|
WHERE archive=true AND type='regular' AND status=ANY('{running, paused, finished}')
|
||||||
|
ORDER by created_at DESC OFFSET $1 LIMIT $2;
|
||||||
|
|
||||||
-- name: get-campaign-stats
|
-- name: get-campaign-stats
|
||||||
-- This query is used to lazy load campaign stats (views, counts, list of lists) given a list of campaign IDs.
|
-- This query is used to lazy load campaign stats (views, counts, list of lists) given a list of campaign IDs.
|
||||||
-- The query returns results in the same order as the given campaign IDs, and for non-existent campaign IDs,
|
-- The query returns results in the same order as the given campaign IDs, and for non-existent campaign IDs,
|
||||||
|
@ -748,6 +756,9 @@ WITH camp AS (
|
||||||
tags=$11::VARCHAR(100)[],
|
tags=$11::VARCHAR(100)[],
|
||||||
messenger=$12,
|
messenger=$12,
|
||||||
template_id=$13,
|
template_id=$13,
|
||||||
|
archive=$15,
|
||||||
|
archive_template_id=$16,
|
||||||
|
archive_meta=$17,
|
||||||
updated_at=NOW()
|
updated_at=NOW()
|
||||||
WHERE id = $1 RETURNING id
|
WHERE id = $1 RETURNING id
|
||||||
),
|
),
|
||||||
|
@ -770,6 +781,14 @@ WHERE id=$1;
|
||||||
-- name: update-campaign-status
|
-- name: update-campaign-status
|
||||||
UPDATE campaigns SET status=$2, updated_at=NOW() WHERE id = $1;
|
UPDATE campaigns SET status=$2, updated_at=NOW() WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: update-campaign-archive
|
||||||
|
UPDATE campaigns SET
|
||||||
|
archive=$2,
|
||||||
|
archive_template_id=(CASE WHEN $3 > 0 THEN $3 ELSE archive_template_id END),
|
||||||
|
archive_meta=(CASE WHEN $4::TEXT != '' THEN $4::JSONB ELSE archive_meta END),
|
||||||
|
updated_at=NOW()
|
||||||
|
WHERE id=$1;
|
||||||
|
|
||||||
-- name: delete-campaign
|
-- name: delete-campaign
|
||||||
DELETE FROM campaigns WHERE id=$1;
|
DELETE FROM campaigns WHERE id=$1;
|
||||||
|
|
||||||
|
@ -1009,3 +1028,4 @@ WITH sub AS (
|
||||||
SELECT id FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END
|
SELECT id FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END
|
||||||
)
|
)
|
||||||
DELETE FROM bounces WHERE subscriber_id = (SELECT id FROM sub);
|
DELETE FROM bounces WHERE subscriber_id = (SELECT id FROM sub);
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,11 @@ CREATE TABLE campaigns (
|
||||||
max_subscriber_id INT NOT NULL DEFAULT 0,
|
max_subscriber_id INT NOT NULL DEFAULT 0,
|
||||||
last_subscriber_id INT NOT NULL DEFAULT 0,
|
last_subscriber_id INT NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Publishing.
|
||||||
|
archive BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
archive_template_id INTEGER REFERENCES templates(id) ON DELETE SET DEFAULT DEFAULT 1,
|
||||||
|
archive_meta JSONB NOT NULL DEFAULT '{}',
|
||||||
|
|
||||||
started_at TIMESTAMP WITH TIME ZONE,
|
started_at TIMESTAMP WITH TIME ZONE,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<title>{{ .Campaign.Subject }}</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<base target="_blank">
|
<base target="_blank">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #F0F1F3;
|
background-color: #F0F1F3;
|
||||||
|
|
|
@ -134,6 +134,20 @@ input[disabled] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.archive .date {
|
||||||
|
display: block;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
.archive li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#btn-back {
|
#btn-back {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
21
static/public/templates/archive.html
Normal file
21
static/public/templates/archive.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{{ define "archive" }}
|
||||||
|
{{ template "header" .}}
|
||||||
|
<section>
|
||||||
|
<h2>{{ L.T "public.archiveTitle" }}</h2>
|
||||||
|
|
||||||
|
<ul class="archive">
|
||||||
|
{{ range $c := .Data.Campaigns }}
|
||||||
|
<li>
|
||||||
|
<a href="{{ $c.URL }}">{{ $c.Subject }}</a>
|
||||||
|
<span class="date">{{ $c.CreatedAt.Time.Format "Mon, 02 Jan 2006" }}</span>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{ if not .Data.Campaigns }}
|
||||||
|
{{ L.T "public.archiveEmpty" }}
|
||||||
|
{{ end }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{ template "footer" .}}
|
||||||
|
{{ end }}
|
Loading…
Reference in a new issue