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