mirror of
https://github.com/knadh/listmonk.git
synced 2025-09-13 01:44:43 +08:00
Fix inconsistent behaviour in campaign scheduling on the UI.
- Fix status/button state management issues when `Send at` was toggled under various scenarios. - Allow paused campaigns to be edited and turned into scheduled campaigns. - Add Cypress UI tests for unscheduling.
This commit is contained in:
parent
fbc27ae4b2
commit
a5f8b28cb1
7 changed files with 56 additions and 25 deletions
|
@ -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');
|
||||
|
|
|
@ -126,6 +126,10 @@ section {
|
|||
background-color: $primary;
|
||||
}
|
||||
|
||||
.has-text-primary {
|
||||
color: $primary !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
background: $white;
|
||||
box-shadow: 2px 2px 0 #f3f3f3;
|
||||
|
|
|
@ -44,8 +44,8 @@
|
|||
</b-button>
|
||||
</b-field>
|
||||
<b-field expanded v-if="canUnSchedule">
|
||||
<b-button expanded @click="unscheduleCampaign" :loading="loading.campaigns" type="is-primary"
|
||||
icon-left="clock-start" data-cy="btn-unschedule">
|
||||
<b-button expanded @click="$utils.confirm(null, unscheduleCampaign)" :loading="loading.campaigns"
|
||||
type="is-primary" icon-left="clock-start" data-cy="btn-unschedule">
|
||||
{{ $t('campaigns.unSchedule') }}
|
||||
</b-button>
|
||||
</b-field>
|
||||
|
@ -127,8 +127,8 @@
|
|||
<br />
|
||||
<b-field v-if="form.sendLater" data-cy="send_at"
|
||||
:message="form.sendAtDate ? $utils.duration(Date(), form.sendAtDate) : ''">
|
||||
<b-datetimepicker v-model="form.sendAtDate" :disabled="!canEdit"
|
||||
:placeholder="$t('campaigns.dateAndTime')" icon="calendar-clock"
|
||||
<b-datetimepicker v-model="form.sendAtDate" :disabled="!canEdit" required editable mobile-native
|
||||
position="is-top-right" :placeholder="$t('campaigns.dateAndTime')" icon="calendar-clock"
|
||||
:timepicker="{ hourFormat: '24' }" :datetime-formatter="formatDateTime"
|
||||
horizontal-time-picker />
|
||||
</b-field>
|
||||
|
@ -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() {
|
||||
|
|
|
@ -52,16 +52,14 @@
|
|||
</router-link>
|
||||
</p>
|
||||
<p v-if="isSheduled(props.row)">
|
||||
<b-tooltip :label="$t('scheduled')" type="is-dark">
|
||||
<span class="is-size-7 has-text-grey scheduled">
|
||||
<b-icon icon="alarm" size="is-small" />
|
||||
<span v-if="!isDone(props.row) && !isRunning(props.row)">
|
||||
{{ $utils.duration(new Date(), props.row.sendAt, true) }}
|
||||
<br />
|
||||
</span>
|
||||
{{ $utils.niceDate(props.row.sendAt, true) }}
|
||||
<span class="is-size-7 has-text-grey scheduled">
|
||||
<b-icon icon="alarm" size="is-small" />
|
||||
<span v-if="!isDone(props.row) && !isRunning(props.row)">
|
||||
{{ $utils.duration(new Date(), props.row.sendAt, true) }}
|
||||
<br />
|
||||
</span>
|
||||
</b-tooltip>
|
||||
{{ $utils.niceDate(props.row.sendAt, true) }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
10
queries.sql
10
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
|
||||
|
|
Loading…
Add table
Reference in a new issue