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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
@ -215,6 +216,10 @@ func handleCreateCampaign(c echo.Context) error {
|
|||
o = c
|
||||
}
|
||||
|
||||
if o.ArchiveTemplateID == 0 {
|
||||
o.ArchiveTemplateID = o.TemplateID
|
||||
}
|
||||
|
||||
out, err := app.core.CreateCampaign(o.Campaign, o.ListIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -294,6 +299,31 @@ func handleUpdateCampaignStatus(c echo.Context) error {
|
|||
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.
|
||||
// Only scheduled campaigns that have not started yet can be deleted.
|
||||
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)
|
||||
}
|
||||
|
||||
if len(c.ArchiveMeta) == 0 {
|
||||
c.ArchiveMeta = json.RawMessage("{}")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -131,6 +131,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
|||
g.POST("/api/campaigns", handleCreateCampaign)
|
||||
g.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
||||
g.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
|
||||
g.PUT("/api/campaigns/:id/archive", handleUpdateCampaignArchive)
|
||||
g.DELETE("/api/campaigns/:id", handleDeleteCampaign)
|
||||
|
||||
g.GET("/api/media", handleGetMedia)
|
||||
|
@ -164,6 +165,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
|||
// Public API endpoints.
|
||||
e.GET("/api/public/lists", handleGetPublicLists)
|
||||
e.POST("/api/public/subscription", handlePublicSubscription)
|
||||
e.GET("/api/public/archive", handleGetCampaignArchives)
|
||||
|
||||
// /public/static/* file server is registered in initHTTPServer().
|
||||
// Public subscriber facing views.
|
||||
|
@ -185,6 +187,8 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
|||
"campUUID", "subUUID")))
|
||||
e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
|
||||
"campUUID", "subUUID")))
|
||||
e.GET("/archive", handleCampaignArchivesPage)
|
||||
e.GET("/archive/:uuid", handleCampaignArchivePage)
|
||||
|
||||
e.GET("/public/custom.css", serveCustomApperance("public.custom_css"))
|
||||
e.GET("/public/custom.js", serveCustomApperance("public.custom_js"))
|
||||
|
|
|
@ -82,6 +82,7 @@ type constants struct {
|
|||
ViewTrackURL string
|
||||
OptinURL string
|
||||
MessageURL string
|
||||
ArchiveURL string
|
||||
MediaProvider string
|
||||
|
||||
BounceWebhooksEnabled bool
|
||||
|
@ -370,6 +371,9 @@ func initConstants() *constants {
|
|||
// url.com/link/{campaign_uuid}/{subscriber_uuid}
|
||||
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
|
||||
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,
|
||||
ViewTrackURL: cs.ViewTrackURL,
|
||||
MessageURL: cs.MessageURL,
|
||||
ArchiveURL: cs.ArchiveURL,
|
||||
UnsubHeader: ko.Bool("privacy.unsubscribe_header"),
|
||||
SlidingWindow: ko.Bool("app.message_sliding_window"),
|
||||
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,
|
||||
campTplID,
|
||||
pq.Int64Array{1},
|
||||
false,
|
||||
campTplID,
|
||||
"{}",
|
||||
); err != nil {
|
||||
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.
|
||||
func (r *runnerDB) GetCampaign(campID int) (*models.Campaign, error) {
|
||||
var out = &models.Campaign{}
|
||||
err := r.queries.GetCampaign.Get(out, campID, nil)
|
||||
err := r.queries.GetCampaign.Get(out, campID, nil, "default")
|
||||
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,
|
||||
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`,
|
||||
{ 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}`,
|
||||
{ loading: models.campaigns });
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
<b-tabs type="is-boxed" :animated="false" v-model="activeTab" @input="onTab">
|
||||
<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">
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
|
@ -172,7 +172,7 @@
|
|||
</section>
|
||||
</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
|
||||
v-model="form.content"
|
||||
:id="data.id"
|
||||
|
@ -198,6 +198,39 @@
|
|||
type="textarea" :disabled="!canEdit" />
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -211,6 +244,8 @@ import htmlToPlainText from 'textversionjs';
|
|||
import ListSelector from '../components/ListSelector.vue';
|
||||
import Editor from '../components/Editor.vue';
|
||||
|
||||
const TABS = ['campaign', 'content', 'archive'];
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
ListSelector,
|
||||
|
@ -247,7 +282,9 @@ export default Vue.extend({
|
|||
// Parsed Date() version of send_at from the API.
|
||||
sendAtDate: null,
|
||||
sendLater: false,
|
||||
|
||||
archive: false,
|
||||
archiveMetaStr: '{}',
|
||||
archiveMeta: {},
|
||||
testEmails: [],
|
||||
},
|
||||
};
|
||||
|
@ -276,7 +313,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
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(() => {
|
||||
window.tinymce.editors[0].focus();
|
||||
});
|
||||
|
@ -284,6 +322,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
onSubmit(typ) {
|
||||
// Validate custom JSON headers.
|
||||
if (this.form.headersStr && this.form.headersStr !== '[]') {
|
||||
try {
|
||||
this.form.headers = JSON.parse(this.form.headersStr);
|
||||
|
@ -295,6 +334,23 @@ export default Vue.extend({
|
|||
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) {
|
||||
case 'create':
|
||||
this.createCampaign();
|
||||
|
@ -315,11 +371,17 @@ export default Vue.extend({
|
|||
...this.form,
|
||||
...data,
|
||||
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.
|
||||
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) {
|
||||
this.form.sendLater = true;
|
||||
this.form.sendAtDate = dayjs(data.sendAt).toDate();
|
||||
|
@ -390,6 +452,9 @@ export default Vue.extend({
|
|||
content_type: this.form.content.contentType,
|
||||
body: this.form.content.body,
|
||||
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';
|
||||
|
@ -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.
|
||||
startCampaign() {
|
||||
if (!this.canStart && !this.canSchedule) {
|
||||
|
@ -451,6 +530,10 @@ export default Vue.extend({
|
|||
return this.data.status === 'draft' && !this.data.sendAt;
|
||||
},
|
||||
|
||||
canArchive() {
|
||||
return this.data.status !== 'cancelled';
|
||||
},
|
||||
|
||||
selectedLists() {
|
||||
if (this.selListIDs.length === 0 || !this.lists.results) {
|
||||
return [];
|
||||
|
@ -481,11 +564,11 @@ export default Vue.extend({
|
|||
mounted() {
|
||||
window.onbeforeunload = () => this.isUnsaved() || null;
|
||||
|
||||
// Fill default form fields.
|
||||
this.form.fromEmail = this.settings['app.from_email'];
|
||||
|
||||
const { id } = this.$route.params;
|
||||
|
||||
// New campaign.
|
||||
const { id } = this.$route.params;
|
||||
if (id === 'new') {
|
||||
this.isNew = true;
|
||||
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Servei desconegut",
|
||||
"bounces.view": "Veure rebots",
|
||||
"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.clicks": "Clics",
|
||||
"campaigns.confirmDelete": "Esborra {name}",
|
||||
|
@ -281,6 +286,8 @@
|
|||
"menu.media": "Mèdia",
|
||||
"menu.newCampaign": "Crea nova",
|
||||
"menu.settings": "Configuració",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "No s'ha trobat el missatge de correu electrònic.",
|
||||
"public.confirmOptinSubTitle": "Confirmació de la subscripció",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Neznámá služba.",
|
||||
"bounces.view": "Zobrazit převzetí",
|
||||
"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.clicks": "Klepnutí",
|
||||
"campaigns.confirmDelete": "Odstranit {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Médium",
|
||||
"menu.newCampaign": "Vytvořit nový",
|
||||
"menu.settings": "Nastavení",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "E-mailová zpráva nebyla nalezena.",
|
||||
"public.confirmOptinSubTitle": "Potvrdit odběr",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Unbekannter Dienst.",
|
||||
"bounces.view": "Bounces anzeigen",
|
||||
"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.clicks": "Klicks",
|
||||
"campaigns.confirmDelete": "Lösche {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Medien",
|
||||
"menu.newCampaign": "Neu Anlegen",
|
||||
"menu.settings": "Einstellungen",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Die E-Mail wurde nicht gefunden.",
|
||||
"public.confirmOptinSubTitle": "Abonnement bestätigen",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Unknown service.",
|
||||
"bounces.view": "View bounces",
|
||||
"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.clicks": "Clicks",
|
||||
"campaigns.confirmDelete": "Delete {name}",
|
||||
|
@ -281,6 +286,8 @@
|
|||
"menu.media": "Media",
|
||||
"menu.newCampaign": "Create new",
|
||||
"menu.settings": "Settings",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "The e-mail message was not found.",
|
||||
"public.confirmOptinSubTitle": "Confirm subscription",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Servicio desconocido.",
|
||||
"bounces.view": "Ver rebotes",
|
||||
"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.clicks": "Clics",
|
||||
"campaigns.confirmDelete": "Eliminar {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Multimedia",
|
||||
"menu.newCampaign": "Crear nueva",
|
||||
"menu.settings": "Configuraciones",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "El mensaje de correo electrónico no fue encontrado",
|
||||
"public.confirmOptinSubTitle": "Confirmar subscripción",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Tuntematon palvelu.",
|
||||
"bounces.view": "Näytä epäonnistuneet toimitukset",
|
||||
"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.clicks": "Klikkaukset",
|
||||
"campaigns.confirmDelete": "Poista {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Media",
|
||||
"menu.newCampaign": "Create new",
|
||||
"menu.settings": "Settings",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Sähköpostiviestiä ei löytynyt",
|
||||
"public.confirmOptinSubTitle": "Vahvista uutiskirjeen tilaus",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Service inconnu.",
|
||||
"bounces.view": "Voir les rebonds",
|
||||
"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.clicks": "Clics",
|
||||
"campaigns.confirmDelete": "Supprimer la campagne {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Fichiers",
|
||||
"menu.newCampaign": "Nouvelle campagne",
|
||||
"menu.settings": "Paramètres",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "La liste de diffusion est introuvable.",
|
||||
"public.confirmOptinSubTitle": "Confirmer votre abonnement",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Ismeretlen szolgáltatás.",
|
||||
"bounces.view": "Visszapattanások megtekintése",
|
||||
"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.clicks": "Kattintások",
|
||||
"campaigns.confirmDelete": "Törlés {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Média",
|
||||
"menu.newCampaign": "Új készítése",
|
||||
"menu.settings": "Beállítások",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Az e-mail üzenet nem található.",
|
||||
"public.confirmOptinSubTitle": "Feliratkozás megerősítése",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Servizio sconosciuto.",
|
||||
"bounces.view": "Visualizza i rimbalzi",
|
||||
"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.clicks": "Clic",
|
||||
"campaigns.confirmDelete": "Cancellare {nome}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Media",
|
||||
"menu.newCampaign": "Creare nuovo",
|
||||
"menu.settings": "Impostazioni",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Newsletter impossibile da trovare.",
|
||||
"public.confirmOptinSubTitle": "Confermare l'iscrizione",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "不明のサービス。",
|
||||
"bounces.view": "バウンスビュー",
|
||||
"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.clicks": "クリック",
|
||||
"campaigns.confirmDelete": "削除 {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "メディア",
|
||||
"menu.newCampaign": "新規作成",
|
||||
"menu.settings": "設定",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "メールのメッセージが見つかりませんでした。",
|
||||
"public.confirmOptinSubTitle": "サブスクリプション確認",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Unknown service.",
|
||||
"bounces.view": "View bounces",
|
||||
"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.clicks": "ക്ലീക്കുകൾ",
|
||||
"campaigns.confirmDelete": "{name} നീക്കം ചെയ്യുക",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "മീഡിയ",
|
||||
"menu.newCampaign": "പുതിയത് തുടങ്ങുക",
|
||||
"menu.settings": "ക്രമീകരണങ്ങൾ",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "ഇ-മെയിൽ കണ്ടെത്താനായില്ല.",
|
||||
"public.confirmOptinSubTitle": "വരിക്കാരനാകുന്നത് സ്ഥിരീകരിക്കുക",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Onbekende service.",
|
||||
"bounces.view": "Zie bounces",
|
||||
"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.clicks": "Kliks",
|
||||
"campaigns.confirmDelete": "Verwijder {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Media",
|
||||
"menu.newCampaign": "Nieuwe aanmaken",
|
||||
"menu.settings": "Instellingen",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Het e-mailbericht werd niet gevonden.",
|
||||
"public.confirmOptinSubTitle": "Bevestig inschrijving",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Nieznane usługi.",
|
||||
"bounces.view": "Zobacz odbicia",
|
||||
"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.clicks": "Kliknięcia",
|
||||
"campaigns.confirmDelete": "Usuń {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Media",
|
||||
"menu.newCampaign": "Utwórz nową",
|
||||
"menu.settings": "Ustawienia",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Wiadomość email nie została znaleziona.",
|
||||
"public.confirmOptinSubTitle": "Potwierdź subskrypcję",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Serviço desconhecido.",
|
||||
"bounces.view": "Ver bounces",
|
||||
"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.clicks": "Cliques",
|
||||
"campaigns.confirmDelete": "Excluir {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Mídia",
|
||||
"menu.newCampaign": "Criar nova",
|
||||
"menu.settings": "Configurações",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "A mensagem do e-mail não foi encontrada.",
|
||||
"public.confirmOptinSubTitle": "Confirmar a assinatura",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Unknown service.",
|
||||
"bounces.view": "View bounces",
|
||||
"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.clicks": "Cliques",
|
||||
"campaigns.confirmDelete": "Eliminar {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Mídia",
|
||||
"menu.newCampaign": "Criar nova",
|
||||
"menu.settings": "Definições",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "A mensagem de email não foi encontrada.",
|
||||
"public.confirmOptinSubTitle": "Confirmar subscrição",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Serviciu necunoscut.",
|
||||
"bounces.view": "Vizualizeaz[ respingeri",
|
||||
"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.clicks": "Clickuri",
|
||||
"campaigns.confirmDelete": "Sterge {nume}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Media",
|
||||
"menu.newCampaign": "Creaza nou",
|
||||
"menu.settings": "Setări",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Mesajul emailului nu a fost găsit.",
|
||||
"public.confirmOptinSubTitle": "Confirmă abonarea",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Неизвестная услуга.",
|
||||
"bounces.view": "Просмотр отскоков",
|
||||
"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.clicks": "Клики",
|
||||
"campaigns.confirmDelete": "Удалить {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Медиа",
|
||||
"menu.newCampaign": "Создать новую",
|
||||
"menu.settings": "Параметры",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Письмо не было найдено.",
|
||||
"public.confirmOptinSubTitle": "Подтверждение подписки",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Unknown service.",
|
||||
"bounces.view": "View bounces",
|
||||
"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.clicks": "Tıklama",
|
||||
"campaigns.confirmDelete": "Sil {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Medya",
|
||||
"menu.newCampaign": "Yeni oluştur",
|
||||
"menu.settings": "Ayarlar",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "E-posta mesajı bulunamadı.",
|
||||
"public.confirmOptinSubTitle": "Üyeliği doğrula",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "Dịch vụ không xác định.",
|
||||
"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.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.clicks": "Số lần nhấp chuột",
|
||||
"campaigns.confirmDelete": "Xóa {name}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "Dữ liệu truyền thông",
|
||||
"menu.newCampaign": "Tạo mới",
|
||||
"menu.settings": "Cài đặt",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "Tin nhắn e-mail không được tìm thấy.",
|
||||
"public.confirmOptinSubTitle": "Xác nhận đăng ký",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "未知的服务。",
|
||||
"bounces.view": "查看退回邮",
|
||||
"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.clicks": "点击次数",
|
||||
"campaigns.confirmDelete": "删除{名称}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "媒体",
|
||||
"menu.newCampaign": "创建新的",
|
||||
"menu.settings": "设置",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "未找到电子邮件。",
|
||||
"public.confirmOptinSubTitle": "确认订阅",
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"bounces.unknownService": "未知的服務。",
|
||||
"bounces.view": "查看退回郵",
|
||||
"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.clicks": "點擊次數",
|
||||
"campaigns.confirmDelete": "刪除{名稱}",
|
||||
|
@ -282,6 +287,8 @@
|
|||
"menu.media": "媒體",
|
||||
"menu.newCampaign": "創建新的",
|
||||
"menu.settings": "設置",
|
||||
"public.archiveEmpty": "No archived messages yet.",
|
||||
"public.archiveTitle": "Mailing list archive",
|
||||
"public.blocklisted": "Permanently unsubscribed.",
|
||||
"public.campaignNotFound": "未找到電子郵件。",
|
||||
"public.confirmOptinSubTitle": "確認訂閱",
|
||||
|
|
|
@ -16,6 +16,9 @@ const (
|
|||
CampaignAnalyticsViews = "views"
|
||||
CampaignAnalyticsClicks = "clicks"
|
||||
CampaignAnalyticsBounces = "bounces"
|
||||
|
||||
campaignTplDefault = "default"
|
||||
campaignTplArchive = "archive"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
var uu interface{}
|
||||
if uuid != "" {
|
||||
|
@ -66,7 +91,7 @@ func (c *Core) GetCampaign(id int, uuid string) (models.Campaign, error) {
|
|||
}
|
||||
|
||||
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 {
|
||||
c.log.Printf("error fetching campaign: %v", err)
|
||||
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 {
|
||||
return models.Campaign{}, echo.NewHTTPError(http.StatusBadRequest,
|
||||
c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
|
||||
|
||||
}
|
||||
|
||||
for i := 0; i < len(out); i++ {
|
||||
|
@ -113,6 +137,23 @@ func (c *Core) GetCampaignForPreview(id, tplID int) (models.Campaign, error) {
|
|||
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.
|
||||
func (c *Core) CreateCampaign(o models.Campaign, listIDs []int) (models.Campaign, error) {
|
||||
uu, err := uuid.NewV4()
|
||||
|
@ -139,6 +180,9 @@ func (c *Core) CreateCampaign(o models.Campaign, listIDs []int) (models.Campaign
|
|||
o.Messenger,
|
||||
o.TemplateID,
|
||||
pq.Array(listIDs),
|
||||
o.Archive,
|
||||
o.ArchiveTemplateID,
|
||||
o.ArchiveMeta,
|
||||
); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
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)),
|
||||
o.Messenger,
|
||||
o.TemplateID,
|
||||
pq.Array(listIDs))
|
||||
pq.Array(listIDs),
|
||||
o.Archive,
|
||||
o.ArchiveTemplateID,
|
||||
o.ArchiveMeta)
|
||||
if err != nil {
|
||||
c.log.Printf("error updating campaign: %v", err)
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Core) DeleteCampaign(id int) error {
|
||||
res, err := c.q.DeleteCampaign.Exec(id)
|
||||
|
|
|
@ -125,6 +125,7 @@ type Config struct {
|
|||
OptinURL string
|
||||
MessageURL string
|
||||
ViewTrackURL string
|
||||
ArchiveURL string
|
||||
UnsubHeader bool
|
||||
|
||||
// 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 {
|
||||
return fmt.Sprintf(m.cfg.MessageURL, c.UUID, msg.Subscriber.UUID)
|
||||
},
|
||||
"ArchiveURL": func() string {
|
||||
return m.cfg.ArchiveURL
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
if _, err := db.Exec(`
|
||||
INSERT INTO settings (key, value) VALUES
|
||||
|
|
|
@ -248,9 +248,13 @@ type Campaign struct {
|
|||
Headers Headers `db:"headers" json:"headers"`
|
||||
TemplateID int `db:"template_id" json:"template_id"`
|
||||
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 string `db:"template_body" json:"-"`
|
||||
ArchiveTemplateBody string `db:"archive_template_body" json:"-"`
|
||||
Tpl *template.Template `json:"-"`
|
||||
SubjectTpl *txttpl.Template `json:"-"`
|
||||
AltBodyTpl *template.Template `json:"-"`
|
||||
|
@ -474,6 +478,26 @@ func (camps Campaigns) LoadStats(stmt *sqlx.Stmt) error {
|
|||
// CompileTemplate compiles a campaign body template into its base
|
||||
// template and sets the resultant template to Campaign.Tpl.
|
||||
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.
|
||||
body := c.TemplateBody
|
||||
for _, r := range regTplFuncs {
|
||||
|
@ -511,21 +535,6 @@ func (c *Campaign) CompileTemplate(f template.FuncMap) error {
|
|||
}
|
||||
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, "{{") {
|
||||
b := c.AltBody.String
|
||||
for _, r := range regTplFuncs {
|
||||
|
|
|
@ -61,6 +61,7 @@ type Queries struct {
|
|||
GetCampaignForPreview *sqlx.Stmt `query:"get-campaign-for-preview"`
|
||||
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
||||
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,
|
||||
// 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"`
|
||||
UpdateCampaignStatus *sqlx.Stmt `query:"update-campaign-status"`
|
||||
UpdateCampaignCounts *sqlx.Stmt `query:"update-campaign-counts"`
|
||||
UpdateCampaignArchive *sqlx.Stmt `query:"update-campaign-archive"`
|
||||
RegisterCampaignView *sqlx.Stmt `query:"register-campaign-view"`
|
||||
DeleteCampaign *sqlx.Stmt `query:"delete-campaign"`
|
||||
|
||||
|
|
28
queries.sql
28
queries.sql
|
@ -473,8 +473,8 @@ counts AS (
|
|||
AND subscribers.status='enabled'
|
||||
),
|
||||
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)
|
||||
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)
|
||||
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), $15, $16, $17
|
||||
RETURNING id
|
||||
)
|
||||
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,
|
||||
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.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,
|
||||
(
|
||||
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.*,
|
||||
COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body
|
||||
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;
|
||||
|
||||
-- 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
|
||||
-- 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,
|
||||
|
@ -748,6 +756,9 @@ WITH camp AS (
|
|||
tags=$11::VARCHAR(100)[],
|
||||
messenger=$12,
|
||||
template_id=$13,
|
||||
archive=$15,
|
||||
archive_template_id=$16,
|
||||
archive_meta=$17,
|
||||
updated_at=NOW()
|
||||
WHERE id = $1 RETURNING id
|
||||
),
|
||||
|
@ -770,6 +781,14 @@ WHERE id=$1;
|
|||
-- name: update-campaign-status
|
||||
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
|
||||
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
|
||||
)
|
||||
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,
|
||||
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,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ .Campaign.Subject }}</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<base target="_blank">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #F0F1F3;
|
||||
|
|
|
@ -134,6 +134,20 @@ input[disabled] {
|
|||
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 {
|
||||
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