diff --git a/frontend/cypress/e2e/campaigns.cy.js b/frontend/cypress/e2e/campaigns.cy.js
index a3f25444..9a531d94 100644
--- a/frontend/cypress/e2e/campaigns.cy.js
+++ b/frontend/cypress/e2e/campaigns.cy.js
@@ -114,6 +114,18 @@ describe('Campaigns', () => {
cy.get('tbody td[data-label=Status] .tag.scheduled');
});
+ it('Unschedules campaign', () => {
+ cy.get('td[data-label=Status] a').eq(1).click();
+ cy.wait(250);
+ cy.get('button[data-cy=btn-unschedule]').click();
+ cy.get('.modal button.is-primary:eq(0)').click();
+ cy.wait(250);
+ cy.visit('/admin/campaigns');
+
+ // Check if the status label has the inner text `Draft`.
+ cy.get('td[data-label=Status] .tag.draft').should('have.length', 1);
+ });
+
it('Switches formats', () => {
cy.resetDB();
cy.loginAndVisit('/admin/campaigns');
diff --git a/frontend/src/assets/style.scss b/frontend/src/assets/style.scss
index 1856e7aa..0bcd02f9 100644
--- a/frontend/src/assets/style.scss
+++ b/frontend/src/assets/style.scss
@@ -126,6 +126,10 @@ section {
background-color: $primary;
}
+.has-text-primary {
+ color: $primary !important;
+}
+
.box {
background: $white;
box-shadow: 2px 2px 0 #f3f3f3;
diff --git a/frontend/src/views/Campaign.vue b/frontend/src/views/Campaign.vue
index 6ff49a17..b37df27b 100644
--- a/frontend/src/views/Campaign.vue
+++ b/frontend/src/views/Campaign.vue
@@ -44,8 +44,8 @@
-
+
{{ $t('campaigns.unSchedule') }}
@@ -127,8 +127,8 @@
-
@@ -469,11 +469,6 @@ export default Vue.extend({
}
return f;
});
-
- if (data.sendAt !== null) {
- this.form.sendLater = true;
- this.form.sendAtDate = dayjs(data.sendAt).toDate();
- }
});
},
@@ -553,11 +548,16 @@ export default Vue.extend({
typMsg = 'campaigns.started';
}
+ if (!this.form.sendAtDate) {
+ this.form.sendLater = false;
+ }
+
// This promise is used by startCampaign to first save before starting.
return new Promise((resolve) => {
this.$api.updateCampaign(this.data.id, data).then((d) => {
this.data = d;
this.form.archiveSlug = d.archiveSlug;
+
this.$utils.toast(this.$t(typMsg, { name: d.name }));
resolve();
});
@@ -613,7 +613,6 @@ export default Vue.extend({
unscheduleCampaign() {
this.$api.changeCampaignStatus(this.data.id, 'draft').then((d) => {
this.data = d;
- this.form.archiveSlug = d.archiveSlug;
});
},
},
@@ -627,15 +626,15 @@ export default Vue.extend({
},
canSchedule() {
- return this.data.status === 'draft' && this.data.sendAt;
+ return (this.data.status === 'draft' || this.data.status === 'paused') && (this.form.sendLater && this.form.sendAtDate);
},
canUnSchedule() {
- return this.data.status === 'scheduled' && this.data.sendAt;
+ return this.data.status === 'scheduled';
},
canStart() {
- return this.data.status === 'draft' || this.data.status === 'paused';
+ return (this.data.status === 'draft' || this.data.status === 'paused') && !this.form.sendLater;
},
canArchive() {
@@ -671,6 +670,16 @@ export default Vue.extend({
selectedLists() {
this.form.lists = this.selectedLists;
},
+
+ 'data.sendAt': function () {
+ if (this.data.sendAt !== null) {
+ this.form.sendLater = true;
+ this.form.sendAtDate = dayjs(this.data.sendAt).toDate();
+ } else {
+ this.form.sendLater = false;
+ this.form.sendAtDate = null;
+ }
+ },
},
mounted() {
diff --git a/frontend/src/views/Campaigns.vue b/frontend/src/views/Campaigns.vue
index 02b84965..efdd4c45 100644
--- a/frontend/src/views/Campaigns.vue
+++ b/frontend/src/views/Campaigns.vue
@@ -52,16 +52,14 @@
-
-
-
-
- {{ $utils.duration(new Date(), props.row.sendAt, true) }}
-
-
- {{ $utils.niceDate(props.row.sendAt, true) }}
+
+
+
+ {{ $utils.duration(new Date(), props.row.sendAt, true) }}
+
-
+ {{ $utils.niceDate(props.row.sendAt, true) }}
+
diff --git a/i18n/en.json b/i18n/en.json
index 426c5ce5..78802181 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -61,7 +61,7 @@
"campaigns.notFound": "Campaign not found.",
"campaigns.onlyActiveCancel": "Only active campaigns can be cancelled.",
"campaigns.onlyActivePause": "Only active campaigns can be paused.",
- "campaigns.onlyDraftAsScheduled": "Only draft campaigns can be scheduled.",
+ "campaigns.onlyDraftAsScheduled": "Only draft or paused campaigns can be scheduled.",
"campaigns.onlyPausedDraft": "Only paused campaigns and drafts can be started.",
"campaigns.onlyScheduledAsDraft": "Only scheduled campaigns can be saved as drafts.",
"campaigns.pause": "Pause",
diff --git a/internal/core/campaigns.go b/internal/core/campaigns.go
index cf3fbbd3..c586ae23 100644
--- a/internal/core/campaigns.go
+++ b/internal/core/campaigns.go
@@ -255,7 +255,7 @@ func (c *Core) UpdateCampaignStatus(id int, status string) (models.Campaign, err
errMsg = c.i18n.T("campaigns.onlyScheduledAsDraft")
}
case models.CampaignStatusScheduled:
- if cm.Status != models.CampaignStatusDraft {
+ if cm.Status != models.CampaignStatusDraft && cm.Status != models.CampaignStatusPaused {
errMsg = c.i18n.T("campaigns.onlyDraftAsScheduled")
}
if !cm.SendAt.Valid {
diff --git a/queries.sql b/queries.sql
index c6ac6cba..a0ce3341 100644
--- a/queries.sql
+++ b/queries.sql
@@ -880,7 +880,15 @@ UPDATE campaigns SET
WHERE id=$1;
-- name: update-campaign-status
-UPDATE campaigns SET status=$2, updated_at=NOW() WHERE id = $1;
+UPDATE campaigns SET
+ status=(
+ CASE
+ WHEN send_at IS NOT NULL AND $2 = 'running' THEN 'scheduled'
+ ELSE $2::campaign_status
+ END
+ ),
+ updated_at=NOW()
+WHERE id = $1;
-- name: update-campaign-archive
UPDATE campaigns SET