WIP: Add tests
This commit is contained in:
parent
039feef938
commit
570a81f966
51
Makefile
51
Makefile
|
@ -17,36 +17,25 @@ deps:
|
||||||
go get -u github.com/knadh/stuffbin/...
|
go get -u github.com/knadh/stuffbin/...
|
||||||
cd frontend && yarn install
|
cd frontend && yarn install
|
||||||
|
|
||||||
# Build the backend to ./listmonk.
|
# Run the JS frontend server in dev mode.
|
||||||
.PHONY: build
|
.PHONY: run-frontend
|
||||||
build:
|
run-frontend:
|
||||||
go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}'" cmd/*.go
|
export VUE_APP_VERSION="${VERSION}" && cd frontend && yarn serve
|
||||||
|
|
||||||
# Run the backend.
|
|
||||||
.PHONY: run
|
|
||||||
run: build
|
|
||||||
./${BIN}
|
|
||||||
|
|
||||||
# Build the JS frontend into frontend/dist.
|
# Build the JS frontend into frontend/dist.
|
||||||
.PHONY: build-frontend
|
.PHONY: build-frontend
|
||||||
build-frontend:
|
build-frontend:
|
||||||
export VUE_APP_VERSION="${VERSION}" && cd frontend && yarn build
|
export VUE_APP_VERSION="${VERSION}" && cd frontend && yarn build
|
||||||
|
|
||||||
# Run the JS frontend server in dev mode.
|
# Run the backend.
|
||||||
.PHONY: run-frontend
|
.PHONY: run
|
||||||
run-frontend:
|
run: build
|
||||||
export VUE_APP_VERSION="${VERSION}" && cd frontend && yarn serve
|
./${BIN}
|
||||||
|
|
||||||
# Run Go tests.
|
# Build the backend to ./listmonk.
|
||||||
.PHONY: test
|
.PHONY: build
|
||||||
test:
|
build:
|
||||||
go test ./...
|
go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}'" cmd/*.go
|
||||||
|
|
||||||
# Bundle all static assets including the JS frontend into the ./listmonk binary
|
|
||||||
# using stuffbin (installed with make deps).
|
|
||||||
.PHONY: dist
|
|
||||||
dist: build build-frontend
|
|
||||||
stuffbin -a stuff -in ${BIN} -out ${BIN} ${STATIC}
|
|
||||||
|
|
||||||
# pack-releases runns stuffbin packing on the given binary. This is used
|
# pack-releases runns stuffbin packing on the given binary. This is used
|
||||||
# in the .goreleaser post-build hook.
|
# in the .goreleaser post-build hook.
|
||||||
|
@ -54,6 +43,12 @@ dist: build build-frontend
|
||||||
pack-bin:
|
pack-bin:
|
||||||
stuffbin -a stuff -in ${BIN} -out ${BIN} ${STATIC}
|
stuffbin -a stuff -in ${BIN} -out ${BIN} ${STATIC}
|
||||||
|
|
||||||
|
# Bundle all static assets including the JS frontend into the ./listmonk binary
|
||||||
|
# using stuffbin (installed with make deps).
|
||||||
|
.PHONY: dist
|
||||||
|
dist: build build-frontend
|
||||||
|
stuffbin -a stuff -in ${BIN} -out ${BIN} ${STATIC}
|
||||||
|
|
||||||
# Use goreleaser to do a dry run producing local builds.
|
# Use goreleaser to do a dry run producing local builds.
|
||||||
.PHONY: release-dry
|
.PHONY: release-dry
|
||||||
release-dry:
|
release-dry:
|
||||||
|
@ -63,3 +58,13 @@ release-dry:
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release:
|
release:
|
||||||
goreleaser --parallelism 1 --rm-dist --skip-validate
|
goreleaser --parallelism 1 --rm-dist --skip-validate
|
||||||
|
|
||||||
|
# Opens the cypress frontend tests UI.
|
||||||
|
.PHONY: open-frontend-tests
|
||||||
|
open-frontend-tests:
|
||||||
|
cd frontend && ./node_modules/cypress/bin/cypress open
|
||||||
|
|
||||||
|
# Run Go tests.
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
|
@ -37,9 +37,17 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerHandlers registers HTTP handlers.
|
// registerHandlers registers HTTP handlers.
|
||||||
func registerHTTPHandlers(e *echo.Echo) {
|
func registerHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
// Group of private handlers with BasicAuth.
|
// Group of private handlers with BasicAuth.
|
||||||
g := e.Group("", middleware.BasicAuth(basicAuth))
|
var g *echo.Group
|
||||||
|
|
||||||
|
if len(app.constants.AdminUsername) == 0 ||
|
||||||
|
len(app.constants.AdminPassword) == 0 {
|
||||||
|
g = e.Group("")
|
||||||
|
} else {
|
||||||
|
g = e.Group("", middleware.BasicAuth(basicAuth))
|
||||||
|
}
|
||||||
|
|
||||||
g.GET("/", handleIndexPage)
|
g.GET("/", handleIndexPage)
|
||||||
g.GET("/api/health", handleHealthCheck)
|
g.GET("/api/health", handleHealthCheck)
|
||||||
g.GET("/api/config", handleGetServerConfig)
|
g.GET("/api/config", handleGetServerConfig)
|
||||||
|
|
|
@ -487,7 +487,7 @@ func initHTTPServer(app *App) *echo.Echo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register all HTTP handlers.
|
// Register all HTTP handlers.
|
||||||
registerHTTPHandlers(srv)
|
registerHTTPHandlers(srv, app)
|
||||||
|
|
||||||
// Start the server.
|
// Start the server.
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
@ -12,6 +12,7 @@ In `main.js`, Buefy and vue-i18n are attached globally. In addition:
|
||||||
|
|
||||||
Some constants are defined in `constants.js`.
|
Some constants are defined in `constants.js`.
|
||||||
|
|
||||||
|
|
||||||
## APIs and states
|
## APIs and states
|
||||||
The project uses a global `vuex` state to centrally store the responses to pretty much all APIs (eg: fetch lists, campaigns etc.) except for a few exceptions. These are called `models` and have been defined in `constants.js`. The definitions are in `store/index.js`.
|
The project uses a global `vuex` state to centrally store the responses to pretty much all APIs (eg: fetch lists, campaigns etc.) except for a few exceptions. These are called `models` and have been defined in `constants.js`. The definitions are in `store/index.js`.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:9000",
|
||||||
|
"env": {
|
||||||
|
"server_init_command": "pkill -9 listmonk | cd ../ && ./listmonk --install --yes && ./listmonk > /dev/null 2>/dev/null &",
|
||||||
|
"username": "listmonk",
|
||||||
|
"password": "listmonk"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"profile": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"uuid": "0954ba2e-50e4-4847-86f4-c2b8b72dace8",
|
||||||
|
"email": "anon@example.com",
|
||||||
|
"name": "Anon Doe",
|
||||||
|
"attribs": {
|
||||||
|
"city": "Bengaluru",
|
||||||
|
"good": true,
|
||||||
|
"type": "unknown"
|
||||||
|
},
|
||||||
|
"status": "enabled",
|
||||||
|
"created_at": "2021-02-20T15:52:16.251648+05:30",
|
||||||
|
"updated_at": "2021-02-20T15:52:16.251648+05:30"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"subscription_status": "unconfirmed",
|
||||||
|
"name": "Opt-in list",
|
||||||
|
"type": "public",
|
||||||
|
"created_at": "2021-02-20T15:52:16.251648+05:30"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"campaign_views": [],
|
||||||
|
"link_clicks": []
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
email,name,attributes
|
||||||
|
user0@mail.com,First0 Last0,"{""age"": 29, ""city"": ""Bangalore"", ""clientId"": ""DAXX79""}"
|
||||||
|
user1@mail.com,First1 Last1,"{""age"": 43, ""city"": ""Bangalore"", ""clientId"": ""DAXX71""}"
|
||||||
|
user2@mail.com,First2 Last2,"{""age"": 47, ""city"": ""Bangalore"", ""clientId"": ""DAXX70""}"
|
||||||
|
user3@mail.com,First3 Last3,"{""age"": 67, ""city"": ""Bangalore"", ""clientId"": ""DAXX32""}"
|
||||||
|
user4@mail.com,First4 Last4,"{""age"": 63, ""city"": ""Bangalore"", ""clientId"": ""DAXX30""}"
|
||||||
|
user5@mail.com,First5 Last5,"{""age"": 69, ""city"": ""Bangalore"", ""clientId"": ""DAXX64""}"
|
||||||
|
user6@mail.com,First6 Last6,"{""age"": 68, ""city"": ""Bangalore"", ""clientId"": ""DAXX22""}"
|
||||||
|
user7@mail.com,First7 Last7,"{""age"": 56, ""city"": ""Bangalore"", ""clientId"": ""DAXX54""}"
|
||||||
|
user8@mail.com,First8 Last8,"{""age"": 58, ""city"": ""Bangalore"", ""clientId"": ""DAXX65""}"
|
||||||
|
user9@mail.com,First9 Last9,"{""age"": 51, ""city"": ""Bangalore"", ""clientId"": ""DAXX66""}"
|
||||||
|
user10@mail.com,First10 Last10,"{""age"": 53, ""city"": ""Bangalore"", ""clientId"": ""DAXX31""}"
|
||||||
|
user11@mail.com,First11 Last11,"{""age"": 46, ""city"": ""Bangalore"", ""clientId"": ""DAXX59""}"
|
||||||
|
user12@mail.com,First12 Last12,"{""age"": 41, ""city"": ""Bangalore"", ""clientId"": ""DAXX80""}"
|
||||||
|
user13@mail.com,First13 Last13,"{""age"": 27, ""city"": ""Bangalore"", ""clientId"": ""DAXX96""}"
|
||||||
|
user14@mail.com,First14 Last14,"{""age"": 51, ""city"": ""Bangalore"", ""clientId"": ""DAXX22""}"
|
||||||
|
user15@mail.com,First15 Last15,"{""age"": 31, ""city"": ""Bangalore"", ""clientId"": ""DAXX97""}"
|
||||||
|
user16@mail.com,First16 Last16,"{""age"": 59, ""city"": ""Bangalore"", ""clientId"": ""DAXX41""}"
|
||||||
|
user17@mail.com,First17 Last17,"{""age"": 29, ""city"": ""Bangalore"", ""clientId"": ""DAXX93""}"
|
||||||
|
user18@mail.com,First18 Last18,"{""age"": 39, ""city"": ""Bangalore"", ""clientId"": ""DAXX35""}"
|
||||||
|
user19@mail.com,First19 Last19,"{""age"": 67, ""city"": ""Bangalore"", ""clientId"": ""DAXX21""}"
|
||||||
|
user20@mail.com,First20 Last20,"{""age"": 66, ""city"": ""Bangalore"", ""clientId"": ""DAXX56""}"
|
||||||
|
user21@mail.com,First21 Last21,"{""age"": 39, ""city"": ""Bangalore"", ""clientId"": ""DAXX26""}"
|
||||||
|
user22@mail.com,First22 Last22,"{""age"": 44, ""city"": ""Bangalore"", ""clientId"": ""DAXX98""}"
|
||||||
|
user23@mail.com,First23 Last23,"{""age"": 66, ""city"": ""Bangalore"", ""clientId"": ""DAXX64""}"
|
||||||
|
user24@mail.com,First24 Last24,"{""age"": 48, ""city"": ""Bangalore"", ""clientId"": ""DAXX41""}"
|
||||||
|
user25@mail.com,First25 Last25,"{""age"": 38, ""city"": ""Bangalore"", ""clientId"": ""DAXX80""}"
|
||||||
|
user26@mail.com,First26 Last26,"{""age"": 27, ""city"": ""Bangalore"", ""clientId"": ""DAXX26""}"
|
||||||
|
user27@mail.com,First27 Last27,"{""age"": 59, ""city"": ""Bangalore"", ""clientId"": ""DAXX55""}"
|
||||||
|
user28@mail.com,First28 Last28,"{""age"": 49, ""city"": ""Bangalore"", ""clientId"": ""DAXX45""}"
|
||||||
|
user29@mail.com,First29 Last29,"{""age"": 45, ""city"": ""Bangalore"", ""clientId"": ""DAXX74""}"
|
||||||
|
user30@mail.com,First30 Last30,"{""age"": 47, ""city"": ""Bangalore"", ""clientId"": ""DAXX27""}"
|
||||||
|
user31@mail.com,First31 Last31,"{""age"": 21, ""city"": ""Bangalore"", ""clientId"": ""DAXX37""}"
|
||||||
|
user32@mail.com,First32 Last32,"{""age"": 21, ""city"": ""Bangalore"", ""clientId"": ""DAXX50""}"
|
||||||
|
user33@mail.com,First33 Last33,"{""age"": 70, ""city"": ""Bangalore"", ""clientId"": ""DAXX29""}"
|
||||||
|
user34@mail.com,First34 Last34,"{""age"": 59, ""city"": ""Bangalore"", ""clientId"": ""DAXX95""}"
|
||||||
|
user35@mail.com,First35 Last35,"{""age"": 36, ""city"": ""Bangalore"", ""clientId"": ""DAXX79""}"
|
||||||
|
user36@mail.com,First36 Last36,"{""age"": 47, ""city"": ""Bangalore"", ""clientId"": ""DAXX30""}"
|
||||||
|
user37@mail.com,First37 Last37,"{""age"": 36, ""city"": ""Bangalore"", ""clientId"": ""DAXX92""}"
|
||||||
|
user38@mail.com,First38 Last38,"{""age"": 29, ""city"": ""Bangalore"", ""clientId"": ""DAXX48""}"
|
||||||
|
user39@mail.com,First39 Last39,"{""age"": 23, ""city"": ""Bangalore"", ""clientId"": ""DAXX12""}"
|
||||||
|
user40@mail.com,First40 Last40,"{""age"": 39, ""city"": ""Bangalore"", ""clientId"": ""DAXX40""}"
|
||||||
|
user41@mail.com,First41 Last41,"{""age"": 41, ""city"": ""Bangalore"", ""clientId"": ""DAXX51""}"
|
||||||
|
user42@mail.com,First42 Last42,"{""age"": 22, ""city"": ""Bangalore"", ""clientId"": ""DAXX49""}"
|
||||||
|
user43@mail.com,First43 Last43,"{""age"": 68, ""city"": ""Bangalore"", ""clientId"": ""DAXX58""}"
|
||||||
|
user44@mail.com,First44 Last44,"{""age"": 45, ""city"": ""Bangalore"", ""clientId"": ""DAXX15""}"
|
||||||
|
user45@mail.com,First45 Last45,"{""age"": 44, ""city"": ""Bangalore"", ""clientId"": ""DAXX75""}"
|
||||||
|
user46@mail.com,First46 Last46,"{""age"": 42, ""city"": ""Bangalore"", ""clientId"": ""DAXX99""}"
|
||||||
|
user47@mail.com,First47 Last47,"{""age"": 61, ""city"": ""Bangalore"", ""clientId"": ""DAXX39""}"
|
||||||
|
user48@mail.com,First48 Last48,"{""age"": 57, ""city"": ""Bangalore"", ""clientId"": ""DAXX13""}"
|
||||||
|
user49@mail.com,First49 Last49,"{""age"": 28, ""city"": ""Bangalore"", ""clientId"": ""DAXX97""}"
|
||||||
|
user50@mail.com,First50 Last50,"{""age"": 61, ""city"": ""Bangalore"", ""clientId"": ""DAXX75""}"
|
||||||
|
user51@mail.com,First51 Last51,"{""age"": 27, ""city"": ""Bangalore"", ""clientId"": ""DAXX55""}"
|
||||||
|
user52@mail.com,First52 Last52,"{""age"": 62, ""city"": ""Bangalore"", ""clientId"": ""DAXX35""}"
|
||||||
|
user53@mail.com,First53 Last53,"{""age"": 24, ""city"": ""Bangalore"", ""clientId"": ""DAXX67""}"
|
||||||
|
user54@mail.com,First54 Last54,"{""age"": 25, ""city"": ""Bangalore"", ""clientId"": ""DAXX36""}"
|
||||||
|
user55@mail.com,First55 Last55,"{""age"": 39, ""city"": ""Bangalore"", ""clientId"": ""DAXX74""}"
|
||||||
|
user56@mail.com,First56 Last56,"{""age"": 53, ""city"": ""Bangalore"", ""clientId"": ""DAXX28""}"
|
||||||
|
user57@mail.com,First57 Last57,"{""age"": 32, ""city"": ""Bangalore"", ""clientId"": ""DAXX36""}"
|
||||||
|
user58@mail.com,First58 Last58,"{""age"": 64, ""city"": ""Bangalore"", ""clientId"": ""DAXX44""}"
|
||||||
|
user59@mail.com,First59 Last59,"{""age"": 47, ""city"": ""Bangalore"", ""clientId"": ""DAXX65""}"
|
||||||
|
user60@mail.com,First60 Last60,"{""age"": 62, ""city"": ""Bangalore"", ""clientId"": ""DAXX11""}"
|
||||||
|
user61@mail.com,First61 Last61,"{""age"": 24, ""city"": ""Bangalore"", ""clientId"": ""DAXX55""}"
|
||||||
|
user62@mail.com,First62 Last62,"{""age"": 61, ""city"": ""Bangalore"", ""clientId"": ""DAXX49""}"
|
||||||
|
user63@mail.com,First63 Last63,"{""age"": 52, ""city"": ""Bangalore"", ""clientId"": ""DAXX83""}"
|
||||||
|
user64@mail.com,First64 Last64,"{""age"": 38, ""city"": ""Bangalore"", ""clientId"": ""DAXX16""}"
|
||||||
|
user65@mail.com,First65 Last65,"{""age"": 48, ""city"": ""Bangalore"", ""clientId"": ""DAXX54""}"
|
||||||
|
user66@mail.com,First66 Last66,"{""age"": 35, ""city"": ""Bangalore"", ""clientId"": ""DAXX74""}"
|
||||||
|
user67@mail.com,First67 Last67,"{""age"": 70, ""city"": ""Bangalore"", ""clientId"": ""DAXX22""}"
|
||||||
|
user68@mail.com,First68 Last68,"{""age"": 21, ""city"": ""Bangalore"", ""clientId"": ""DAXX98""}"
|
||||||
|
user69@mail.com,First69 Last69,"{""age"": 46, ""city"": ""Bangalore"", ""clientId"": ""DAXX24""}"
|
||||||
|
user70@mail.com,First70 Last70,"{""age"": 58, ""city"": ""Bangalore"", ""clientId"": ""DAXX75""}"
|
||||||
|
user71@mail.com,First71 Last71,"{""age"": 50, ""city"": ""Bangalore"", ""clientId"": ""DAXX57""}"
|
||||||
|
user72@mail.com,First72 Last72,"{""age"": 63, ""city"": ""Bangalore"", ""clientId"": ""DAXX30""}"
|
||||||
|
user73@mail.com,First73 Last73,"{""age"": 54, ""city"": ""Bangalore"", ""clientId"": ""DAXX77""}"
|
||||||
|
user74@mail.com,First74 Last74,"{""age"": 67, ""city"": ""Bangalore"", ""clientId"": ""DAXX91""}"
|
||||||
|
user75@mail.com,First75 Last75,"{""age"": 61, ""city"": ""Bangalore"", ""clientId"": ""DAXX30""}"
|
||||||
|
user76@mail.com,First76 Last76,"{""age"": 50, ""city"": ""Bangalore"", ""clientId"": ""DAXX28""}"
|
||||||
|
user77@mail.com,First77 Last77,"{""age"": 62, ""city"": ""Bangalore"", ""clientId"": ""DAXX41""}"
|
||||||
|
user78@mail.com,First78 Last78,"{""age"": 66, ""city"": ""Bangalore"", ""clientId"": ""DAXX18""}"
|
||||||
|
user79@mail.com,First79 Last79,"{""age"": 40, ""city"": ""Bangalore"", ""clientId"": ""DAXX89""}"
|
||||||
|
user80@mail.com,First80 Last80,"{""age"": 21, ""city"": ""Bangalore"", ""clientId"": ""DAXX72""}"
|
||||||
|
user81@mail.com,First81 Last81,"{""age"": 43, ""city"": ""Bangalore"", ""clientId"": ""DAXX31""}"
|
||||||
|
user82@mail.com,First82 Last82,"{""age"": 33, ""city"": ""Bangalore"", ""clientId"": ""DAXX89""}"
|
||||||
|
user83@mail.com,First83 Last83,"{""age"": 38, ""city"": ""Bangalore"", ""clientId"": ""DAXX88""}"
|
||||||
|
user84@mail.com,First84 Last84,"{""age"": 24, ""city"": ""Bangalore"", ""clientId"": ""DAXX77""}"
|
||||||
|
user85@mail.com,First85 Last85,"{""age"": 27, ""city"": ""Bangalore"", ""clientId"": ""DAXX40""}"
|
||||||
|
user86@mail.com,First86 Last86,"{""age"": 67, ""city"": ""Bangalore"", ""clientId"": ""DAXX46""}"
|
||||||
|
user87@mail.com,First87 Last87,"{""age"": 20, ""city"": ""Bangalore"", ""clientId"": ""DAXX53""}"
|
||||||
|
user88@mail.com,First88 Last88,"{""age"": 45, ""city"": ""Bangalore"", ""clientId"": ""DAXX79""}"
|
||||||
|
user89@mail.com,First89 Last89,"{""age"": 31, ""city"": ""Bangalore"", ""clientId"": ""DAXX11""}"
|
||||||
|
user90@mail.com,First90 Last90,"{""age"": 51, ""city"": ""Bangalore"", ""clientId"": ""DAXX71""}"
|
||||||
|
user91@mail.com,First91 Last91,"{""age"": 49, ""city"": ""Bangalore"", ""clientId"": ""DAXX20""}"
|
||||||
|
user92@mail.com,First92 Last92,"{""age"": 26, ""city"": ""Bangalore"", ""clientId"": ""DAXX20""}"
|
||||||
|
user93@mail.com,First93 Last93,"{""age"": 67, ""city"": ""Bangalore"", ""clientId"": ""DAXX64""}"
|
||||||
|
user94@mail.com,First94 Last94,"{""age"": 60, ""city"": ""Bangalore"", ""clientId"": ""DAXX53""}"
|
||||||
|
user95@mail.com,First95 Last95,"{""age"": 64, ""city"": ""Bangalore"", ""clientId"": ""DAXX91""}"
|
||||||
|
user96@mail.com,First96 Last96,"{""age"": 27, ""city"": ""Bangalore"", ""clientId"": ""DAXX53""}"
|
||||||
|
user97@mail.com,First97 Last97,"{""age"": 29, ""city"": ""Bangalore"", ""clientId"": ""DAXX46""}"
|
||||||
|
user98@mail.com,First98 Last98,"{""age"": 26, ""city"": ""Bangalore"", ""clientId"": ""DAXX49""}"
|
||||||
|
user99@mail.com,First99 Last99,"{""age"": 49, ""city"": ""Bangalore"", ""clientId"": ""DAXX26""}"
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
describe('Subscribers', () => {
|
||||||
|
it('Opens campaigns page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/campaigns');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Counts campaigns', () => {
|
||||||
|
cy.get('tbody td[data-label=Status]').should('have.length', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edits campaign', () => {
|
||||||
|
cy.get('td[data-label=Status] a').click();
|
||||||
|
|
||||||
|
// Fill fields.
|
||||||
|
cy.get('input[name=name]').clear().type('new-name');
|
||||||
|
cy.get('input[name=subject]').clear().type('new-subject');
|
||||||
|
cy.get('input[name=from_email]').clear().type('new <from@email>');
|
||||||
|
|
||||||
|
// Change the list.
|
||||||
|
cy.get('.list-selector a.delete').click();
|
||||||
|
cy.get('.list-selector input').click();
|
||||||
|
cy.get('.list-selector .autocomplete a').eq(1).click();
|
||||||
|
|
||||||
|
// Clear and redo tags.
|
||||||
|
cy.get('input[name=tags]').type('{backspace}new-tag{enter}');
|
||||||
|
|
||||||
|
// Enable schedule.
|
||||||
|
cy.get('[data-cy=btn-send-later] .check').click();
|
||||||
|
cy.get('.datepicker input').click();
|
||||||
|
cy.get('.datepicker-header .control:nth-child(2) select').select((new Date().getFullYear() + 1).toString());
|
||||||
|
cy.get('.datepicker-body a.is-selectable:first').click();
|
||||||
|
cy.get('body').click(1, 1);
|
||||||
|
|
||||||
|
// Switch to content tab.
|
||||||
|
cy.get('.b-tabs nav a').eq(1).click();
|
||||||
|
|
||||||
|
// Switch format to plain text.
|
||||||
|
cy.get('label[data-cy=check-plain]').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
|
||||||
|
// Enter body value.
|
||||||
|
cy.get('textarea[name=content]').clear().type('new-content');
|
||||||
|
cy.get('button[data-cy=btn-save]').click();
|
||||||
|
|
||||||
|
// Schedule.
|
||||||
|
cy.get('button[data-cy=btn-schedule]').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
|
||||||
|
cy.wait(250);
|
||||||
|
|
||||||
|
// Verify the changes.
|
||||||
|
cy.request('/api/campaigns/1').should((response) => {
|
||||||
|
const { data } = response.body;
|
||||||
|
expect(data.status).to.equal('scheduled');
|
||||||
|
expect(data.name).to.equal('new-name');
|
||||||
|
expect(data.subject).to.equal('new-subject');
|
||||||
|
expect(data.content_type).to.equal('plain');
|
||||||
|
expect(data.altbody).to.equal(null);
|
||||||
|
expect(data.send_at).to.not.equal(null);
|
||||||
|
expect(data.body).to.equal('new-content');
|
||||||
|
|
||||||
|
expect(data.lists.length).to.equal(1);
|
||||||
|
expect(data.lists[0].id).to.equal(2);
|
||||||
|
expect(data.tags.length).to.equal(1);
|
||||||
|
expect(data.tags[0]).to.equal('new-tag');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('tbody td[data-label=Status] .tag.scheduled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clones campaign', () => {
|
||||||
|
for (let n = 0; n < 3; n++) {
|
||||||
|
// Clone the campaign.
|
||||||
|
cy.get('[data-cy=btn-clone]').first().click();
|
||||||
|
cy.get('.modal input').clear().type(`clone${n}`).click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.clickMenu('all-campaigns');
|
||||||
|
cy.wait(100);
|
||||||
|
|
||||||
|
// Verify the newly created row.
|
||||||
|
cy.get('tbody td[data-label="Name"]').first().contains(`clone${n}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Searches campaigns', () => {
|
||||||
|
cy.get('input[name=query]').clear().type('clone2{enter}');
|
||||||
|
cy.get('tbody tr').its('length').should('eq', 1);
|
||||||
|
cy.get('tbody td[data-label="Name"]').first().contains('clone2');
|
||||||
|
cy.get('input[name=query]').clear().type('{enter}');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Deletes campaign', () => {
|
||||||
|
// Delete all visible lists.
|
||||||
|
cy.get('tbody tr').each(() => {
|
||||||
|
cy.get('tbody a[data-cy=btn-delete]').first().click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm deletion.
|
||||||
|
cy.get('table tr.is-empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Adds new campaigns', () => {
|
||||||
|
const lists = [[1], [1, 2]];
|
||||||
|
const cTypes = ['richtext', 'html', 'plain'];
|
||||||
|
|
||||||
|
let n = 0;
|
||||||
|
cTypes.forEach((c) => {
|
||||||
|
lists.forEach((l) => {
|
||||||
|
// Click the 'new button'
|
||||||
|
cy.get('[data-cy=btn-new]').click();
|
||||||
|
cy.wait(100);
|
||||||
|
|
||||||
|
// Fill fields.
|
||||||
|
cy.get('input[name=name]').clear().type(`name${n}`);
|
||||||
|
cy.get('input[name=subject]').clear().type(`subject${n}`);
|
||||||
|
|
||||||
|
l.forEach(() => {
|
||||||
|
cy.get('.list-selector input').click();
|
||||||
|
cy.get('.list-selector .autocomplete a').first().click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add tags.
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
cy.get('input[name=tags]').type(`tag${i}{enter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hit 'Continue'.
|
||||||
|
cy.get('button[data-cy=btn-continue]').click();
|
||||||
|
cy.wait(250);
|
||||||
|
|
||||||
|
// Insert content.
|
||||||
|
cy.get('.ql-editor').type(`hello${n} \{\{ .Subscriber.Name \}\}`, { parseSpecialCharSequences: false });
|
||||||
|
cy.get('.ql-editor').type('{enter}');
|
||||||
|
cy.get('.ql-editor').type('\{\{ .Subscriber.Attribs.city \}\}', { parseSpecialCharSequences: false });
|
||||||
|
|
||||||
|
// Select content type.
|
||||||
|
cy.get(`label[data-cy=check-${c}]`).click();
|
||||||
|
|
||||||
|
// If it's not richtext, there's a "you'll lose formatting" prompt.
|
||||||
|
if (c !== 'richtext') {
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save.
|
||||||
|
cy.get('button[data-cy=btn-save]').click();
|
||||||
|
|
||||||
|
cy.clickMenu('all-campaigns');
|
||||||
|
cy.wait(250);
|
||||||
|
|
||||||
|
// Verify the newly created campaign in the table.
|
||||||
|
cy.get('tbody td[data-label="Name"]').first().contains(`name${n}`);
|
||||||
|
cy.get('tbody td[data-label="Name"]').first().contains(`subject${n}`);
|
||||||
|
cy.get('tbody td[data-label="Lists"]').first().then(($el) => {
|
||||||
|
cy.wrap($el).find('li').should('have.length', l.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
n++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch the campaigns API and verfiy the values that couldn't be verified on the table UI.
|
||||||
|
cy.request('/api/campaigns?order=asc&order_by=created_at').should((response) => {
|
||||||
|
const { data } = response.body;
|
||||||
|
expect(data.total).to.equal(lists.length * cTypes.length);
|
||||||
|
|
||||||
|
let n = 0;
|
||||||
|
cTypes.forEach((c) => {
|
||||||
|
lists.forEach((l) => {
|
||||||
|
expect(data.results[n].content_type).to.equal(c);
|
||||||
|
expect(data.results[n].lists.map((ls) => ls.id)).to.deep.equal(l);
|
||||||
|
n++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Starts and cancels campaigns', () => {
|
||||||
|
for (let n = 1; n <= 2; n++) {
|
||||||
|
cy.get(`tbody tr:nth-child(${n}) [data-cy=btn-start]`).click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.get(`tbody tr:nth-child(${n}) td[data-label=Status] .tag.running`);
|
||||||
|
|
||||||
|
if (n > 1) {
|
||||||
|
cy.get(`tbody tr:nth-child(${n}) [data-cy=btn-cancel]`).click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.get(`tbody tr:nth-child(${n}) td[data-label=Status] .tag.cancelled`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Sorts campaigns', () => {
|
||||||
|
const asc = [5, 6, 7, 8, 9, 10];
|
||||||
|
const desc = [10, 9, 8, 7, 6, 5];
|
||||||
|
const cases = ['cy-name', 'cy-timestamp'];
|
||||||
|
|
||||||
|
cases.forEach((c) => {
|
||||||
|
cy.sortTable(`thead th.${c}`, asc);
|
||||||
|
cy.wait(250);
|
||||||
|
cy.sortTable(`thead th.${c}`, desc);
|
||||||
|
cy.wait(250);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,28 @@
|
||||||
|
describe('Dashboard', () => {
|
||||||
|
it('Opens dashboard', () => {
|
||||||
|
cy.loginAndVisit('/');
|
||||||
|
|
||||||
|
// List counts.
|
||||||
|
cy.get('[data-cy=lists]')
|
||||||
|
.should('contain', '2 Lists')
|
||||||
|
.and('contain', '1 Public')
|
||||||
|
.and('contain', '1 Private')
|
||||||
|
.and('contain', '1 Single opt-in')
|
||||||
|
.and('contain', '1 Double opt-in');
|
||||||
|
|
||||||
|
// Campaign counts.
|
||||||
|
cy.get('[data-cy=campaigns]')
|
||||||
|
.should('contain', '1 Campaign')
|
||||||
|
.and('contain', '1 draft');
|
||||||
|
|
||||||
|
// Subscriber counts.
|
||||||
|
cy.get('[data-cy=subscribers]')
|
||||||
|
.should('contain', '2 Subscribers')
|
||||||
|
.and('contain', '0 Blocklisted')
|
||||||
|
.and('contain', '0 Orphans');
|
||||||
|
|
||||||
|
// Message count.
|
||||||
|
cy.get('[data-cy=messages]')
|
||||||
|
.should('contain', '0 Messages sent');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
describe('Forms', () => {
|
||||||
|
it('Opens forms page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/lists/forms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Checks form URL', () => {
|
||||||
|
cy.get('a[data-cy=url]').contains('http://localhost:9000');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Checks public lists', () => {
|
||||||
|
cy.get('ul[data-cy=lists] li')
|
||||||
|
.should('contain', 'Opt-in list')
|
||||||
|
.its('length')
|
||||||
|
.should('eq', 1);
|
||||||
|
|
||||||
|
cy.get('[data-cy=form] pre').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Selects public list', () => {
|
||||||
|
// Click the list checkbox.
|
||||||
|
cy.get('ul[data-cy=lists] .checkbox').click();
|
||||||
|
|
||||||
|
// Make sure the <pre> form HTML has appeared.
|
||||||
|
cy.get('[data-cy=form] pre').then(($pre) => {
|
||||||
|
// Check that the ID of the list in the checkbox appears in the HTML.
|
||||||
|
cy.get('ul[data-cy=lists] input').then(($inp) => {
|
||||||
|
cy.wrap($pre).contains($inp.val());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click the list checkbox.
|
||||||
|
cy.get('ul[data-cy=lists] .checkbox').click();
|
||||||
|
cy.get('[data-cy=form] pre').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
describe('Import', () => {
|
||||||
|
it('Opens import page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/subscribers/import');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Imports subscribers', () => {
|
||||||
|
const cases = [
|
||||||
|
{ mode: 'check-subscribe', status: 'enabled', count: 102 },
|
||||||
|
{ mode: 'check-blocklist', status: 'blocklisted', count: 102 },
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((c) => {
|
||||||
|
cy.get(`[data-cy=${c.mode}] .check`).click();
|
||||||
|
|
||||||
|
if (c.status === 'enabled') {
|
||||||
|
cy.get('.list-selector input').click();
|
||||||
|
cy.get('.list-selector .autocomplete a').first().click();
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.fixture('subs.csv').then((data) => {
|
||||||
|
cy.get('input[type="file"]').attachFile({
|
||||||
|
fileContent: data.toString(),
|
||||||
|
fileName: 'subs.csv',
|
||||||
|
mimeType: 'text/csv',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('button.is-primary').click();
|
||||||
|
cy.get('section.wrap .has-text-success');
|
||||||
|
cy.get('button.is-primary').click();
|
||||||
|
cy.wait(100);
|
||||||
|
|
||||||
|
// Verify that 100 (+2 default) subs are imported.
|
||||||
|
cy.loginAndVisit('/subscribers');
|
||||||
|
cy.wait(100);
|
||||||
|
cy.get('[data-cy=count]').then(($el) => {
|
||||||
|
cy.expect(parseInt($el.text().trim())).to.equal(c.count);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('tbody td[data-label=Status]').each(($el) => {
|
||||||
|
cy.wrap($el).find(`.tag.${c.status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.loginAndVisit('/subscribers/import');
|
||||||
|
cy.wait(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,130 @@
|
||||||
|
describe('Lists', () => {
|
||||||
|
it('Opens lists page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/lists');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Counts subscribers in default lists', () => {
|
||||||
|
cy.get('tbody td[data-label=Subscribers]').contains('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Creates campaign for list', () => {
|
||||||
|
cy.get('tbody a[data-cy=btn-campaign]').first().click();
|
||||||
|
cy.location('pathname').should('contain', '/campaigns/new');
|
||||||
|
cy.get('.list-tags .tag').contains('Default list');
|
||||||
|
|
||||||
|
cy.clickMenu('lists', 'all-lists');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Creates opt-in campaign for list', () => {
|
||||||
|
cy.get('tbody a[data-cy=btn-send-optin-campaign]').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.location('pathname').should('contain', '/campaigns/2');
|
||||||
|
|
||||||
|
cy.clickMenu('lists', 'all-lists');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Checks individual subscribers in lists', () => {
|
||||||
|
const subs = [{ listID: 1, email: 'john@example.com' },
|
||||||
|
{ listID: 2, email: 'anon@example.com' }];
|
||||||
|
|
||||||
|
// Click on each list on the lists page, go the the subscribers page
|
||||||
|
// for that list, and check the subscriber details.
|
||||||
|
subs.forEach((s, n) => {
|
||||||
|
cy.get('tbody td[data-label=Subscribers] a').eq(n).click();
|
||||||
|
cy.location('pathname').should('contain', `/subscribers/lists/${s.listID}`);
|
||||||
|
cy.get('tbody tr').its('length').should('eq', 1);
|
||||||
|
cy.get('tbody td[data-label="E-mail"]').contains(s.email);
|
||||||
|
cy.clickMenu('lists', 'all-lists');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edits lists', () => {
|
||||||
|
// Open the edit popup and edit the default lists.
|
||||||
|
cy.get('[data-cy=btn-edit]').each(($el, n) => {
|
||||||
|
cy.wrap($el).click();
|
||||||
|
cy.get('input[name=name]').clear().type(`list-${n}`);
|
||||||
|
cy.get('select[name=type]').select('public');
|
||||||
|
cy.get('select[name=optin]').select('double');
|
||||||
|
cy.get('input[name=tags]').clear().type(`tag${n}`);
|
||||||
|
cy.get('button[type=submit]').click();
|
||||||
|
});
|
||||||
|
cy.wait(250);
|
||||||
|
|
||||||
|
// Confirm the edits.
|
||||||
|
cy.get('tbody tr').each(($el, n) => {
|
||||||
|
cy.wrap($el).find('td[data-label=Name]').contains(`list-${n}`);
|
||||||
|
cy.wrap($el).find('.tags')
|
||||||
|
.should('contain', 'test')
|
||||||
|
.and('contain', `tag${n}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Deletes lists', () => {
|
||||||
|
// Delete all visible lists.
|
||||||
|
cy.get('tbody tr').each(() => {
|
||||||
|
cy.get('tbody a[data-cy=btn-delete]').first().click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm deletion.
|
||||||
|
cy.get('table tr.is-empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Add new lists.
|
||||||
|
it('Adds new lists', () => {
|
||||||
|
// Open the list form and create lists of multiple type/optin combinations.
|
||||||
|
const types = ['private', 'public'];
|
||||||
|
const optin = ['single', 'double'];
|
||||||
|
|
||||||
|
let n = 0;
|
||||||
|
types.forEach((t) => {
|
||||||
|
optin.forEach((o) => {
|
||||||
|
const name = `list-${t}-${o}-${n}`;
|
||||||
|
|
||||||
|
cy.get('[data-cy=btn-new]').click();
|
||||||
|
cy.get('input[name=name]').type(name);
|
||||||
|
cy.get('select[name=type]').select(t);
|
||||||
|
cy.get('select[name=optin]').select(o);
|
||||||
|
cy.get('input[name=tags]').type(`tag${n}{enter}${t}{enter}${o}{enter}`);
|
||||||
|
cy.get('button[type=submit]').click();
|
||||||
|
|
||||||
|
// Confirm the addition by inspecting the newly created list row.
|
||||||
|
const tr = `tbody tr:nth-child(${n + 1})`;
|
||||||
|
cy.get(`${tr} td[data-label=Name]`).contains(name);
|
||||||
|
cy.get(`${tr} td[data-label=Type] [data-cy=type-${t}]`);
|
||||||
|
cy.get(`${tr} td[data-label=Type] [data-cy=optin-${o}]`);
|
||||||
|
cy.get(`${tr} .tags`)
|
||||||
|
.should('contain', `tag${n}`)
|
||||||
|
.and('contain', t)
|
||||||
|
.and('contain', o);
|
||||||
|
|
||||||
|
n++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Sort lists by clicking on various headers. At this point, there should be four
|
||||||
|
// lists with IDs = [3, 4, 5, 6]. Sort the items be columns and match them with
|
||||||
|
// the expected order of IDs.
|
||||||
|
it('Sorts lists', () => {
|
||||||
|
cy.sortTable('thead th.cy-name', [4, 3, 6, 5]);
|
||||||
|
cy.sortTable('thead th.cy-name', [5, 6, 3, 4]);
|
||||||
|
|
||||||
|
cy.sortTable('thead th.cy-type', [5, 6, 4, 3]);
|
||||||
|
cy.sortTable('thead th.cy-type', [4, 3, 5, 6]);
|
||||||
|
|
||||||
|
cy.sortTable('thead th.cy-created_at', [3, 4, 5, 6]);
|
||||||
|
cy.sortTable('thead th.cy-created_at', [6, 5, 4, 3]);
|
||||||
|
|
||||||
|
cy.sortTable('thead th.cy-updated_at', [3, 4, 5, 6]);
|
||||||
|
cy.sortTable('thead th.cy-updated_at', [6, 5, 4, 3]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,40 @@
|
||||||
|
describe('Templates', () => {
|
||||||
|
it('Opens settings page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Changes some settings', () => {
|
||||||
|
const rootURL = 'http://127.0.0.1:9000';
|
||||||
|
const faveURL = 'http://127.0.0.1:9000/public/static/logo.png';
|
||||||
|
|
||||||
|
cy.get('input[name="app.root_url"]').clear().type(rootURL);
|
||||||
|
cy.get('input[name="app.favicon_url"]').type(faveURL);
|
||||||
|
cy.get('.b-tabs nav a').eq(1).click();
|
||||||
|
cy.get('.tab-item:visible').find('.field').first()
|
||||||
|
.find('button')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Enable / disable SMTP and delete one.
|
||||||
|
cy.get('.b-tabs nav a').eq(4).click();
|
||||||
|
cy.get('.tab-item:visible [data-cy=btn-enable-smtp]').eq(1).click();
|
||||||
|
cy.get('.tab-item:visible [data-cy=btn-delete-smtp]').first().click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy=btn-save]').click();
|
||||||
|
|
||||||
|
cy.wait(250);
|
||||||
|
|
||||||
|
// Verify the changes.
|
||||||
|
cy.request('/api/settings').should((response) => {
|
||||||
|
const { data } = response.body;
|
||||||
|
expect(data['app.root_url']).to.equal(rootURL);
|
||||||
|
expect(data['app.favicon_url']).to.equal(faveURL);
|
||||||
|
expect(data['app.concurrency']).to.equal(9);
|
||||||
|
|
||||||
|
expect(data.smtp.length).to.equal(1);
|
||||||
|
expect(data.smtp[0].enabled).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,219 @@
|
||||||
|
describe('Subscribers', () => {
|
||||||
|
it('Opens subscribers page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/subscribers');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Counts subscribers', () => {
|
||||||
|
cy.get('tbody td[data-label=Status]').its('length').should('eq', 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Searches subscribers', () => {
|
||||||
|
const cases = [
|
||||||
|
{ value: 'john{enter}', count: 1, contains: 'john@example.com' },
|
||||||
|
{ value: 'anon{enter}', count: 1, contains: 'anon@example.com' },
|
||||||
|
{ value: '{enter}', count: 2, contains: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((c) => {
|
||||||
|
cy.get('[data-cy=search]').clear().type(c.value);
|
||||||
|
cy.get('tbody td[data-label=Status]').its('length').should('eq', c.count);
|
||||||
|
if (c.contains) {
|
||||||
|
cy.get('tbody td[data-label=E-mail]').contains(c.contains);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Advanced searches subscribers', () => {
|
||||||
|
cy.get('[data-cy=btn-advanced-search]').click();
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{ value: 'subscribers.attribs->>\'city\'=\'Bengaluru\'', count: 2 },
|
||||||
|
{ value: 'subscribers.attribs->>\'city\'=\'Bengaluru\' AND id=1', count: 1 },
|
||||||
|
{ value: '(subscribers.attribs->>\'good\')::BOOLEAN = true AND name like \'Anon%\'', count: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((c) => {
|
||||||
|
cy.get('[data-cy=query]').clear().type(c.value);
|
||||||
|
cy.get('[data-cy=btn-query]').click();
|
||||||
|
cy.get('tbody td[data-label=Status]').its('length').should('eq', c.count);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-cy=btn-query-reset]').click();
|
||||||
|
cy.get('tbody td[data-label=Status]').its('length').should('eq', 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Does bulk subscriber list add and remove', () => {
|
||||||
|
const cases = [
|
||||||
|
// radio: action to perform, rows: table rows to select and perform on: [expected statuses of those rows after thea action]
|
||||||
|
{ radio: 'check-list-add', lists: [0, 1], rows: { 0: ['unconfirmed', 'unconfirmed'] } },
|
||||||
|
{ radio: 'check-list-unsubscribe', lists: [0, 1], rows: { 0: ['unsubscribed', 'unsubscribed'], 1: ['unsubscribed'] } },
|
||||||
|
{ radio: 'check-list-remove', lists: [0, 1], rows: { 1: [] } },
|
||||||
|
{ radio: 'check-list-add', lists: [0, 1], rows: { 0: ['unsubscribed', 'unsubscribed'], 1: ['unconfirmed', 'unconfirmed'] } },
|
||||||
|
{ radio: 'check-list-remove', lists: [0], rows: { 0: ['unsubscribed'] } },
|
||||||
|
{ radio: 'check-list-add', lists: [0], rows: { 0: ['unconfirmed', 'unsubscribed'] } },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
cases.forEach((c, n) => {
|
||||||
|
// Select one of the 2 subscriber in the table.
|
||||||
|
Object.keys(c.rows).forEach((r) => {
|
||||||
|
cy.get('tbody td.checkbox-cell .checkbox').eq(r).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the 'manage lists' modal.
|
||||||
|
cy.get('[data-cy=btn-manage-lists]').click();
|
||||||
|
|
||||||
|
// Check both lists in the modal.
|
||||||
|
c.lists.forEach((l) => {
|
||||||
|
cy.get('.list-selector input').click();
|
||||||
|
cy.get('.list-selector .autocomplete a').first().click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select the radio option in the modal.
|
||||||
|
cy.get(`[data-cy=${c.radio}] .check`).click();
|
||||||
|
|
||||||
|
// Save.
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
|
||||||
|
// Check the status of the lists on the subscriber.
|
||||||
|
Object.keys(c.rows).forEach((r) => {
|
||||||
|
cy.get('tbody td[data-label=E-mail]').eq(r).find('.tags').then(($el) => {
|
||||||
|
cy.wrap($el).find('.tag').should('have.length', c.rows[r].length);
|
||||||
|
c.rows[r].forEach((status, n) => {
|
||||||
|
// eg: .tag(n).unconfirmed
|
||||||
|
cy.wrap($el).find(`.tag:nth-child(${n + 1}).${status}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Resets subscribers page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/subscribers');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Edits subscribers', () => {
|
||||||
|
const status = ['enabled', 'blocklisted'];
|
||||||
|
const json = '{"string": "hello", "ints": [1,2,3], "null": null, "sub": {"bool": true}}';
|
||||||
|
|
||||||
|
// Collect values being edited on each sub to confirm the changes in the next step
|
||||||
|
// index by their ID shown in the modal.
|
||||||
|
const rows = {};
|
||||||
|
|
||||||
|
// Open the edit popup and edit the default lists.
|
||||||
|
cy.get('[data-cy=btn-edit]').each(($el, n) => {
|
||||||
|
const email = `email-${n}@email.com`;
|
||||||
|
const name = `name-${n}`;
|
||||||
|
|
||||||
|
// Open the edit modal.
|
||||||
|
cy.wrap($el).click();
|
||||||
|
|
||||||
|
// Get the ID from the header and proceed to fill the form.
|
||||||
|
let id = 0;
|
||||||
|
cy.get('[data-cy=id]').then(($el) => {
|
||||||
|
id = $el.text();
|
||||||
|
|
||||||
|
cy.get('input[name=email]').clear().type(email);
|
||||||
|
cy.get('input[name=name]').clear().type(name);
|
||||||
|
cy.get('select[name=status]').select(status[n]);
|
||||||
|
cy.get('.list-selector input').click();
|
||||||
|
cy.get('.list-selector .autocomplete a').first().click();
|
||||||
|
cy.get('textarea[name=attribs]').clear().type(json, { parseSpecialCharSequences: false, delay: 0 });
|
||||||
|
cy.get('.modal-card-foot button[type=submit]').click();
|
||||||
|
|
||||||
|
rows[id] = { email, name, status: status[n] };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm the edits on the table.
|
||||||
|
cy.get('tbody tr').each(($el) => {
|
||||||
|
cy.wrap($el).find('td[data-id]').invoke('attr', 'data-id').then((id) => {
|
||||||
|
cy.wrap($el).find('td[data-label=E-mail]').contains(rows[id].email);
|
||||||
|
cy.wrap($el).find('td[data-label=Name]').contains(rows[id].name);
|
||||||
|
cy.wrap($el).find('td[data-label=Status]').contains(rows[id].status, { matchCase: false });
|
||||||
|
|
||||||
|
// Both lists on the enabled sub should be 'unconfirmed' and the blocklisted one, 'unsubscribed.'
|
||||||
|
cy.wait(250);
|
||||||
|
cy.wrap($el).find(`.tags .${rows[id].status === 'enabled' ? 'unconfirmed' : 'unsubscribed'}`)
|
||||||
|
.its('length').should('eq', 2);
|
||||||
|
cy.wrap($el).find('td[data-label=Lists]').then((l) => {
|
||||||
|
cy.expect(parseInt(l.text().trim())).to.equal(rows[id].status === 'blocklisted' ? 0 : 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Deletes subscribers', () => {
|
||||||
|
// Delete all visible lists.
|
||||||
|
cy.get('tbody tr').each(() => {
|
||||||
|
cy.get('tbody a[data-cy=btn-delete]').first().click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm deletion.
|
||||||
|
cy.get('table tr.is-empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Creates new subscribers', () => {
|
||||||
|
const statuses = ['enabled', 'blocklisted'];
|
||||||
|
const lists = [[1], [2], [1, 2]];
|
||||||
|
const json = '{"string": "hello", "ints": [1,2,3], "null": null, "sub": {"bool": true}}';
|
||||||
|
|
||||||
|
|
||||||
|
// Cycle through each status and each list ID combination and create subscribers.
|
||||||
|
const n = 0;
|
||||||
|
for (let n = 0; n < 6; n++) {
|
||||||
|
const email = `email-${n}@email.com`;
|
||||||
|
const name = `name-${n}`;
|
||||||
|
const status = statuses[(n + 1) % statuses.length];
|
||||||
|
const list = lists[(n + 1) % lists.length];
|
||||||
|
|
||||||
|
cy.get('[data-cy=btn-new]').click();
|
||||||
|
cy.get('input[name=email]').type(email);
|
||||||
|
cy.get('input[name=name]').type(name);
|
||||||
|
cy.get('select[name=status]').select(status);
|
||||||
|
|
||||||
|
list.forEach((l) => {
|
||||||
|
cy.get('.list-selector input').click();
|
||||||
|
cy.get('.list-selector .autocomplete a').first().click();
|
||||||
|
});
|
||||||
|
cy.get('textarea[name=attribs]').clear().type(json, { parseSpecialCharSequences: false, delay: 0 });
|
||||||
|
cy.get('.modal-card-foot button[type=submit]').click();
|
||||||
|
|
||||||
|
// Confirm the addition by inspecting the newly created list row,
|
||||||
|
// which is always the first row in the table.
|
||||||
|
cy.wait(250);
|
||||||
|
const tr = cy.get('tbody tr:nth-child(1)').then(($el) => {
|
||||||
|
cy.wrap($el).find('td[data-label=E-mail]').contains(email);
|
||||||
|
cy.wrap($el).find('td[data-label=Name]').contains(name);
|
||||||
|
cy.wrap($el).find('td[data-label=Status]').contains(status, { matchCase: false });
|
||||||
|
cy.wrap($el).find(`.tags .${status === 'enabled' ? 'unconfirmed' : 'unsubscribed'}`)
|
||||||
|
.its('length').should('eq', list.length);
|
||||||
|
cy.wrap($el).find('td[data-label=Lists]').then((l) => {
|
||||||
|
cy.expect(parseInt(l.text().trim())).to.equal(status === 'blocklisted' ? 0 : list.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Sorts subscribers', () => {
|
||||||
|
const asc = [3, 4, 5, 6, 7, 8];
|
||||||
|
const desc = [8, 7, 6, 5, 4, 3];
|
||||||
|
const cases = ['cy-status', 'cy-email', 'cy-name', 'cy-created_at', 'cy-updated_at'];
|
||||||
|
|
||||||
|
cases.forEach((c) => {
|
||||||
|
cy.sortTable(`thead th.${c}`, asc);
|
||||||
|
cy.wait(100);
|
||||||
|
cy.sortTable(`thead th.${c}`, desc);
|
||||||
|
cy.wait(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,77 @@
|
||||||
|
describe('Templates', () => {
|
||||||
|
it('Opens templates page', () => {
|
||||||
|
cy.resetDB();
|
||||||
|
cy.loginAndVisit('/campaigns/templates');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Counts default templates', () => {
|
||||||
|
cy.get('tbody td[data-label=Name]').should('have.length', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clones template', () => {
|
||||||
|
// Clone the campaign.
|
||||||
|
cy.get('[data-cy=btn-clone]').first().click();
|
||||||
|
cy.get('.modal input').clear().type('cloned').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
|
||||||
|
// Verify the newly created row.
|
||||||
|
cy.get('tbody td[data-label="Name"]').eq(1).contains('cloned');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edits template', () => {
|
||||||
|
cy.get('tbody td.actions [data-cy=btn-edit]').first().click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.get('input[name=name]').clear().type('edited');
|
||||||
|
cy.get('textarea[name=body]').clear().type('<span>test</span> {{ template "content" . }}',
|
||||||
|
{ parseSpecialCharSequences: false, delay: 0 });
|
||||||
|
cy.get('footer.modal-card-foot button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.get('tbody td[data-label="Name"] a').contains('edited');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Previews templates', () => {
|
||||||
|
// Edited one sould have a bare body.
|
||||||
|
cy.get('tbody [data-cy=btn-preview').eq(0).click();
|
||||||
|
cy.wait(500);
|
||||||
|
cy.get('.modal-card-body iframe').iframe(() => {
|
||||||
|
cy.get('span').first().contains('test');
|
||||||
|
cy.get('p').first().contains('Hi there');
|
||||||
|
});
|
||||||
|
cy.get('footer.modal-card-foot button').click();
|
||||||
|
|
||||||
|
// Cloned one should have the full template.
|
||||||
|
cy.get('tbody [data-cy=btn-preview').eq(1).click();
|
||||||
|
cy.wait(500);
|
||||||
|
cy.get('.modal-card-body iframe').iframe(() => {
|
||||||
|
cy.get('.wrap p').first().contains('Hi there');
|
||||||
|
cy.get('.footer a').first().contains('Unsubscribe');
|
||||||
|
});
|
||||||
|
cy.get('footer.modal-card-foot button').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Sets default', () => {
|
||||||
|
cy.get('tbody td.actions').eq(1).find('[data-cy=btn-set-default]').click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
|
||||||
|
// The original default shouldn't have default and the new one should have.
|
||||||
|
cy.get('tbody td.actions').eq(0).then((el) => {
|
||||||
|
cy.wrap(el).find('[data-cy=btn-delete]').should('exist');
|
||||||
|
cy.wrap(el).find('[data-cy=btn-set-default]').should('exist');
|
||||||
|
});
|
||||||
|
cy.get('tbody td.actions').eq(1).then((el) => {
|
||||||
|
cy.wrap(el).find('[data-cy=btn-delete]').should('not.exist');
|
||||||
|
cy.wrap(el).find('[data-cy=btn-set-default]').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Deletes template', () => {
|
||||||
|
cy.get('tbody td.actions [data-cy=btn-delete]').first().click();
|
||||||
|
cy.get('.modal button.is-primary').click();
|
||||||
|
cy.wait(250);
|
||||||
|
cy.get('tbody td.actions').should('have.length', 1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import 'cypress-file-upload';
|
||||||
|
|
||||||
|
Cypress.Commands.add('resetDB', () => {
|
||||||
|
// Although cypress clearly states that a webserver should not be run
|
||||||
|
// from within it, listmonk is killed, the DB reset, and run again
|
||||||
|
// in the background. If the DB is reset without restartin listmonk,
|
||||||
|
// the live Postgres connections in the app throw errors because the
|
||||||
|
// schema changes midway.
|
||||||
|
cy.exec(Cypress.env('server_init_command'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Takes a th class selector of a Buefy table, clicks it sorting the table,
|
||||||
|
// then compares the values of [td.data-id] attri of all the rows in the
|
||||||
|
// table against the given IDs, asserting the expected order of sort.
|
||||||
|
Cypress.Commands.add('sortTable', (theadSelector, ordIDs) => {
|
||||||
|
cy.get(theadSelector).click();
|
||||||
|
cy.get('tbody td[data-id]').each(($el, index) => {
|
||||||
|
expect(ordIDs[index]).to.equal(parseInt($el.attr('data-id')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('loginAndVisit', (url) => {
|
||||||
|
cy.visit(url, {
|
||||||
|
auth: {
|
||||||
|
username: Cypress.env('username'),
|
||||||
|
password: Cypress.env('password'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('clickMenu', (...selectors) => {
|
||||||
|
selectors.forEach((s) => {
|
||||||
|
cy.get(`.menu a[data-cy="${s}"]`).click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.nicknish.co/blog/cypress-targeting-elements-inside-iframes
|
||||||
|
Cypress.Commands.add('iframe', { prevSubject: 'element' }, ($iframe, callback = () => {}) => cy
|
||||||
|
.wrap($iframe)
|
||||||
|
.should((iframe) => expect(iframe.contents().find('body')).to.exist)
|
||||||
|
.then((iframe) => cy.wrap(iframe.contents().find('body')))
|
||||||
|
.within({}, callback));
|
|
@ -0,0 +1,16 @@
|
||||||
|
import './commands';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.server({
|
||||||
|
ignore: (xhr) => {
|
||||||
|
// Ignore the webpack dev server calls that interfere in the tests
|
||||||
|
// when testing with `yarn serve`.
|
||||||
|
if (xhr.url.indexOf('sockjs-node/') > -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the default cypress whitelist filer.
|
||||||
|
return xhr.method === 'GET' && /\.(jsx?|html|css)(\?.*)?$/.test(xhr.url);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
pkill -9 listmonk
|
||||||
|
cd ../
|
||||||
|
./listmonk --install --yes
|
||||||
|
./listmonk > /dev/null 2>/dev/null &
|
|
@ -40,6 +40,8 @@
|
||||||
"@vue/cli-service": "~4.4.0",
|
"@vue/cli-service": "~4.4.0",
|
||||||
"@vue/eslint-config-airbnb": "^5.0.2",
|
"@vue/eslint-config-airbnb": "^5.0.2",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
|
"cypress": "^6.4.0",
|
||||||
|
"cypress-file-upload": "^5.0.2",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
|
|
@ -32,63 +32,63 @@
|
||||||
</b-menu-item><!-- dashboard -->
|
</b-menu-item><!-- dashboard -->
|
||||||
|
|
||||||
<b-menu-item :expanded="activeGroup.lists"
|
<b-menu-item :expanded="activeGroup.lists"
|
||||||
:active="activeGroup.lists"
|
:active="activeGroup.lists" data-cy="lists"
|
||||||
v-on:update:active="(state) => toggleGroup('lists', state)"
|
v-on:update:active="(state) => toggleGroup('lists', state)"
|
||||||
icon="format-list-bulleted-square" :label="$t('globals.terms.lists')">
|
icon="format-list-bulleted-square" :label="$t('globals.terms.lists')">
|
||||||
<b-menu-item :to="{name: 'lists'}" tag="router-link"
|
<b-menu-item :to="{name: 'lists'}" tag="router-link"
|
||||||
:active="activeItem.lists"
|
:active="activeItem.lists" data-cy="all-lists"
|
||||||
icon="format-list-bulleted-square" :label="$t('menu.allLists')"></b-menu-item>
|
icon="format-list-bulleted-square" :label="$t('menu.allLists')"></b-menu-item>
|
||||||
|
|
||||||
<b-menu-item :to="{name: 'forms'}" tag="router-link"
|
<b-menu-item :to="{name: 'forms'}" tag="router-link"
|
||||||
:active="activeItem.forms"
|
:active="activeItem.forms" class="forms"
|
||||||
icon="newspaper-variant-outline" :label="$t('menu.forms')"></b-menu-item>
|
icon="newspaper-variant-outline" :label="$t('menu.forms')"></b-menu-item>
|
||||||
</b-menu-item><!-- lists -->
|
</b-menu-item><!-- lists -->
|
||||||
|
|
||||||
<b-menu-item :expanded="activeGroup.subscribers"
|
<b-menu-item :expanded="activeGroup.subscribers"
|
||||||
:active="activeGroup.subscribers"
|
:active="activeGroup.subscribers" data-cy="subscribers"
|
||||||
v-on:update:active="(state) => toggleGroup('subscribers', state)"
|
v-on:update:active="(state) => toggleGroup('subscribers', state)"
|
||||||
icon="account-multiple" :label="$t('globals.terms.subscribers')">
|
icon="account-multiple" :label="$t('globals.terms.subscribers')">
|
||||||
<b-menu-item :to="{name: 'subscribers'}" tag="router-link"
|
<b-menu-item :to="{name: 'subscribers'}" tag="router-link"
|
||||||
:active="activeItem.subscribers"
|
:active="activeItem.subscribers" data-cy="all-subscribers"
|
||||||
icon="account-multiple" :label="$t('menu.allSubscribers')"></b-menu-item>
|
icon="account-multiple" :label="$t('menu.allSubscribers')"></b-menu-item>
|
||||||
|
|
||||||
<b-menu-item :to="{name: 'import'}" tag="router-link"
|
<b-menu-item :to="{name: 'import'}" tag="router-link"
|
||||||
:active="activeItem.import"
|
:active="activeItem.import" data-cy="import"
|
||||||
icon="file-upload-outline" :label="$t('menu.import')"></b-menu-item>
|
icon="file-upload-outline" :label="$t('menu.import')"></b-menu-item>
|
||||||
</b-menu-item><!-- subscribers -->
|
</b-menu-item><!-- subscribers -->
|
||||||
|
|
||||||
<b-menu-item :expanded="activeGroup.campaigns"
|
<b-menu-item :expanded="activeGroup.campaigns"
|
||||||
:active="activeGroup.campaigns"
|
:active="activeGroup.campaigns" data-cy="campaigns"
|
||||||
v-on:update:active="(state) => toggleGroup('campaigns', state)"
|
v-on:update:active="(state) => toggleGroup('campaigns', state)"
|
||||||
icon="rocket-launch-outline" :label="$t('globals.terms.campaigns')">
|
icon="rocket-launch-outline" :label="$t('globals.terms.campaigns')">
|
||||||
<b-menu-item :to="{name: 'campaigns'}" tag="router-link"
|
<b-menu-item :to="{name: 'campaigns'}" tag="router-link"
|
||||||
:active="activeItem.campaigns"
|
:active="activeItem.campaigns" data-cy="all-campaigns"
|
||||||
icon="rocket-launch-outline" :label="$t('menu.allCampaigns')"></b-menu-item>
|
icon="rocket-launch-outline" :label="$t('menu.allCampaigns')"></b-menu-item>
|
||||||
|
|
||||||
<b-menu-item :to="{name: 'campaign', params: {id: 'new'}}" tag="router-link"
|
<b-menu-item :to="{name: 'campaign', params: {id: 'new'}}" tag="router-link"
|
||||||
:active="activeItem.campaign"
|
:active="activeItem.campaign" data-cy="new-campaign"
|
||||||
icon="plus" :label="$t('menu.newCampaign')"></b-menu-item>
|
icon="plus" :label="$t('menu.newCampaign')"></b-menu-item>
|
||||||
|
|
||||||
<b-menu-item :to="{name: 'media'}" tag="router-link"
|
<b-menu-item :to="{name: 'media'}" tag="router-link"
|
||||||
:active="activeItem.media"
|
:active="activeItem.media" data-cy="media"
|
||||||
icon="image-outline" :label="$t('menu.media')"></b-menu-item>
|
icon="image-outline" :label="$t('menu.media')"></b-menu-item>
|
||||||
|
|
||||||
<b-menu-item :to="{name: 'templates'}" tag="router-link"
|
<b-menu-item :to="{name: 'templates'}" tag="router-link"
|
||||||
:active="activeItem.templates"
|
:active="activeItem.templates" data-cy="templates"
|
||||||
icon="file-image-outline" :label="$t('globals.terms.templates')"></b-menu-item>
|
icon="file-image-outline" :label="$t('globals.terms.templates')"></b-menu-item>
|
||||||
</b-menu-item><!-- campaigns -->
|
</b-menu-item><!-- campaigns -->
|
||||||
|
|
||||||
<b-menu-item :expanded="activeGroup.settings"
|
<b-menu-item :expanded="activeGroup.settings"
|
||||||
:active="activeGroup.settings"
|
:active="activeGroup.settings" data-cy="settings"
|
||||||
v-on:update:active="(state) => toggleGroup('settings', state)"
|
v-on:update:active="(state) => toggleGroup('settings', state)"
|
||||||
icon="cog-outline" :label="$t('menu.settings')">
|
icon="cog-outline" :label="$t('menu.settings')">
|
||||||
|
|
||||||
<b-menu-item :to="{name: 'settings'}" tag="router-link"
|
<b-menu-item :to="{name: 'settings'}" tag="router-link"
|
||||||
:active="activeItem.settings"
|
:active="activeItem.settings" data-cy="all-settings"
|
||||||
icon="cog-outline" :label="$t('menu.settings')"></b-menu-item>
|
icon="cog-outline" :label="$t('menu.settings')"></b-menu-item>
|
||||||
|
|
||||||
<b-menu-item :to="{name: 'logs'}" tag="router-link"
|
<b-menu-item :to="{name: 'logs'}" tag="router-link"
|
||||||
:active="activeItem.logs"
|
:active="activeItem.logs" data-cy="logs"
|
||||||
icon="newspaper-variant-outline" :label="$t('menu.logs')"></b-menu-item>
|
icon="newspaper-variant-outline" :label="$t('menu.logs')"></b-menu-item>
|
||||||
</b-menu-item><!-- settings -->
|
</b-menu-item><!-- settings -->
|
||||||
</b-menu-list>
|
</b-menu-list>
|
||||||
|
|
|
@ -7,13 +7,16 @@
|
||||||
<div>
|
<div>
|
||||||
<b-radio v-model="form.radioFormat"
|
<b-radio v-model="form.radioFormat"
|
||||||
@input="onChangeFormat" :disabled="disabled" name="format"
|
@input="onChangeFormat" :disabled="disabled" name="format"
|
||||||
native-value="richtext">{{ $t('campaigns.richText') }}</b-radio>
|
native-value="richtext"
|
||||||
|
data-cy="check-richtext">{{ $t('campaigns.richText') }}</b-radio>
|
||||||
<b-radio v-model="form.radioFormat"
|
<b-radio v-model="form.radioFormat"
|
||||||
@input="onChangeFormat" :disabled="disabled" name="format"
|
@input="onChangeFormat" :disabled="disabled" name="format"
|
||||||
native-value="html">{{ $t('campaigns.rawHTML') }}</b-radio>
|
native-value="html"
|
||||||
|
data-cy="check-html">{{ $t('campaigns.rawHTML') }}</b-radio>
|
||||||
<b-radio v-model="form.radioFormat"
|
<b-radio v-model="form.radioFormat"
|
||||||
@input="onChangeFormat" :disabled="disabled" name="format"
|
@input="onChangeFormat" :disabled="disabled" name="format"
|
||||||
native-value="plain">{{ $t('campaigns.plainText') }}</b-radio>
|
native-value="plain"
|
||||||
|
data-cy="check-plain">{{ $t('campaigns.plainText') }}</b-radio>
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +45,7 @@
|
||||||
|
|
||||||
<!-- plain text editor //-->
|
<!-- plain text editor //-->
|
||||||
<b-input v-if="form.format === 'plain'" v-model="form.body" @input="onEditorChange"
|
<b-input v-if="form.format === 'plain'" v-model="form.body" @input="onEditorChange"
|
||||||
type="textarea" ref="plainEditor" class="plain-editor" />
|
type="textarea" name="content" ref="plainEditor" class="plain-editor" />
|
||||||
|
|
||||||
<!-- campaign preview //-->
|
<!-- campaign preview //-->
|
||||||
<campaign-preview v-if="isPreviewing"
|
<campaign-preview v-if="isPreviewing"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="field">
|
<div class="field list-selector">
|
||||||
<div :class="['list-tags', ...classes]">
|
<div :class="['list-tags', ...classes]">
|
||||||
<b-taglist>
|
<b-taglist>
|
||||||
<b-tag v-for="l in selectedItems"
|
<b-tag v-for="l in selectedItems"
|
||||||
|
|
|
@ -17,15 +17,15 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="buttons" v-if="isEditing && canEdit">
|
<div class="buttons" v-if="isEditing && canEdit">
|
||||||
<b-button @click="onSubmit" :loading="loading.campaigns"
|
<b-button @click="onSubmit" :loading="loading.campaigns"
|
||||||
type="is-primary" icon-left="content-save-outline">
|
type="is-primary" icon-left="content-save-outline" data-cy="btn-save">
|
||||||
{{ $t('globals.buttons.saveChanges') }}
|
{{ $t('globals.buttons.saveChanges') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button v-if="canStart" @click="startCampaign" :loading="loading.campaigns"
|
<b-button v-if="canStart" @click="startCampaign" :loading="loading.campaigns"
|
||||||
type="is-primary" icon-left="rocket-launch-outline">
|
type="is-primary" icon-left="rocket-launch-outline" data-cy="btn-start">
|
||||||
{{ $t('campaigns.start') }}
|
{{ $t('campaigns.start') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button v-if="canSchedule" @click="startCampaign" :loading="loading.campaigns"
|
<b-button v-if="canSchedule" @click="startCampaign" :loading="loading.campaigns"
|
||||||
type="is-primary" icon-left="clock-start">
|
type="is-primary" icon-left="clock-start" data-cy="btn-schedule">
|
||||||
{{ $t('campaigns.schedule') }}
|
{{ $t('campaigns.schedule') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,17 +42,20 @@
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
<form @submit.prevent="onSubmit">
|
<form @submit.prevent="onSubmit">
|
||||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" :disabled="!canEdit"
|
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
||||||
|
name="name" :disabled="!canEdit"
|
||||||
:placeholder="$t('globals.fields.name')" required></b-input>
|
:placeholder="$t('globals.fields.name')" required></b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('campaigns.subject')" label-position="on-border">
|
<b-field :label="$t('campaigns.subject')" label-position="on-border">
|
||||||
<b-input :maxlength="200" v-model="form.subject" :disabled="!canEdit"
|
<b-input :maxlength="200" v-model="form.subject"
|
||||||
|
name="subject" :disabled="!canEdit"
|
||||||
:placeholder="$t('campaigns.subject')" required></b-input>
|
:placeholder="$t('campaigns.subject')" required></b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('campaigns.fromAddress')" label-position="on-border">
|
<b-field :label="$t('campaigns.fromAddress')" label-position="on-border">
|
||||||
<b-input :maxlength="200" v-model="form.fromEmail" :disabled="!canEdit"
|
<b-input :maxlength="200" v-model="form.fromEmail"
|
||||||
|
name="from_email" :disabled="!canEdit"
|
||||||
:placeholder="$t('campaigns.fromAddressPlaceholder')" required></b-input>
|
:placeholder="$t('campaigns.fromAddressPlaceholder')" required></b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -67,34 +70,34 @@
|
||||||
|
|
||||||
<b-field :label="$tc('globals.terms.template')" label-position="on-border">
|
<b-field :label="$tc('globals.terms.template')" label-position="on-border">
|
||||||
<b-select :placeholder="$tc('globals.terms.template')" v-model="form.templateId"
|
<b-select :placeholder="$tc('globals.terms.template')" v-model="form.templateId"
|
||||||
:disabled="!canEdit" required>
|
name="template" :disabled="!canEdit" required>
|
||||||
<option v-for="t in templates" :value="t.id" :key="t.id">{{ t.name }}</option>
|
<option v-for="t in templates" :value="t.id" :key="t.id">{{ t.name }}</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$tc('globals.terms.messenger')" label-position="on-border">
|
<b-field :label="$tc('globals.terms.messenger')" label-position="on-border">
|
||||||
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger"
|
<b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger"
|
||||||
:disabled="!canEdit" required>
|
name="messenger" :disabled="!canEdit" required>
|
||||||
<option v-for="m in messengers"
|
<option v-for="m in messengers"
|
||||||
:value="m" :key="m">{{ m }}</option>
|
:value="m" :key="m">{{ m }}</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('globals.terms.tags')" label-position="on-border">
|
<b-field :label="$t('globals.terms.tags')" label-position="on-border">
|
||||||
<b-taginput v-model="form.tags" :disabled="!canEdit"
|
<b-taginput v-model="form.tags" name="tags" :disabled="!canEdit"
|
||||||
ellipsis icon="tag-outline" :placeholder="$t('globals.terms.tags')" />
|
ellipsis icon="tag-outline" :placeholder="$t('globals.terms.tags')" />
|
||||||
</b-field>
|
</b-field>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<b-field :label="$t('campaigns.sendLater')">
|
<b-field :label="$t('campaigns.sendLater')" data-cy="btn-send-later">
|
||||||
<b-switch v-model="form.sendLater" :disabled="!canEdit" />
|
<b-switch v-model="form.sendLater" :disabled="!canEdit" />
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<br />
|
<br />
|
||||||
<b-field v-if="form.sendLater"
|
<b-field v-if="form.sendLater" data-cy="send_at"
|
||||||
:message="form.sendAtDate ? $utils.duration(Date(), form.sendAtDate) : ''">
|
:message="form.sendAtDate ? $utils.duration(Date(), form.sendAtDate) : ''">
|
||||||
<b-datetimepicker
|
<b-datetimepicker
|
||||||
v-model="form.sendAtDate"
|
v-model="form.sendAtDate"
|
||||||
|
@ -112,7 +115,9 @@
|
||||||
|
|
||||||
<b-field v-if="isNew">
|
<b-field v-if="isNew">
|
||||||
<b-button native-type="submit" type="is-primary"
|
<b-button native-type="submit" type="is-primary"
|
||||||
:loading="loading.campaigns">{{ $t('campaigns.continue') }}</b-button>
|
:loading="loading.campaigns" data-cy="btn-continue">
|
||||||
|
{{ $t('campaigns.continue') }}
|
||||||
|
</b-button>
|
||||||
</b-field>
|
</b-field>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,13 +8,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column has-text-right">
|
<div class="column has-text-right">
|
||||||
<b-button :to="{name: 'campaign', params:{id: 'new'}}" tag="router-link"
|
<b-button :to="{name: 'campaign', params:{id: 'new'}}" tag="router-link"
|
||||||
type="is-primary" icon-left="plus">{{ $t('globals.buttons.new') }}</b-button>
|
type="is-primary" icon-left="plus" data-cy="btn-new">
|
||||||
|
{{ $t('globals.buttons.new') }}
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form @submit.prevent="getCampaigns">
|
<form @submit.prevent="getCampaigns">
|
||||||
<b-field grouped>
|
<b-field grouped>
|
||||||
<b-input v-model="queryParams.query"
|
<b-input v-model="queryParams.query" name="query"
|
||||||
:placeholder="$t('campaigns.queryPlaceholder')" icon="magnify" ref="query"></b-input>
|
:placeholder="$t('campaigns.queryPlaceholder')" icon="magnify" ref="query"></b-input>
|
||||||
<b-button native-type="submit" type="is-primary" icon-left="magnify"></b-button>
|
<b-button native-type="submit" type="is-primary" icon-left="magnify"></b-button>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
@ -29,7 +31,8 @@
|
||||||
hoverable backend-sorting @sort="onSort">
|
hoverable backend-sorting @sort="onSort">
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<b-table-column class="status" field="status" :label="$t('globals.fields.status')"
|
<b-table-column class="status" field="status" :label="$t('globals.fields.status')"
|
||||||
width="10%" :id="props.row.id" sortable>
|
width="10%" :id="props.row.id" sortable
|
||||||
|
header-class="cy-status" :data-id="props.row.id">
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
|
<router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
|
||||||
|
@ -46,13 +49,14 @@
|
||||||
<span class="is-size-7 has-text-grey scheduled">
|
<span class="is-size-7 has-text-grey scheduled">
|
||||||
<b-icon icon="alarm" size="is-small" />
|
<b-icon icon="alarm" size="is-small" />
|
||||||
{{ $utils.duration(Date(), props.row.sendAt, true) }}
|
{{ $utils.duration(Date(), props.row.sendAt, true) }}
|
||||||
– {{ $utils.niceDate(props.row.sendAt, true) }}
|
<br />{{ $utils.niceDate(props.row.sendAt, true) }}
|
||||||
</span>
|
</span>
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
<b-table-column field="name" :label="$t('globals.fields.name')" sortable width="25%">
|
<b-table-column field="name" :label="$t('globals.fields.name')" sortable width="25%"
|
||||||
|
header-class="cy-name">
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<b-tag v-if="props.row.type !== 'regular'" class="is-small">
|
<b-tag v-if="props.row.type !== 'regular'" class="is-small">
|
||||||
|
@ -78,7 +82,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
<b-table-column field="created_at" :label="$t('campaigns.timestamps')"
|
<b-table-column field="created_at" :label="$t('campaigns.timestamps')"
|
||||||
width="19%" sortable>
|
width="19%" sortable header-class="cy-timestamp">
|
||||||
<div class="fields timestamps" :set="stats = getCampaignStats(props.row)">
|
<div class="fields timestamps" :set="stats = getCampaignStats(props.row)">
|
||||||
<p>
|
<p>
|
||||||
<label>{{ $t('globals.fields.createdAt') }}</label>
|
<label>{{ $t('globals.fields.createdAt') }}</label>
|
||||||
|
@ -136,33 +140,33 @@
|
||||||
<div>
|
<div>
|
||||||
<a href="" v-if="canStart(props.row)"
|
<a href="" v-if="canStart(props.row)"
|
||||||
@click.prevent="$utils.confirm(null,
|
@click.prevent="$utils.confirm(null,
|
||||||
() => changeCampaignStatus(props.row, 'running'))">
|
() => changeCampaignStatus(props.row, 'running'))" data-cy="btn-start">
|
||||||
<b-tooltip :label="$t('campaigns.start')" type="is-dark">
|
<b-tooltip :label="$t('campaigns.start')" type="is-dark">
|
||||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" v-if="canPause(props.row)"
|
<a href="" v-if="canPause(props.row)"
|
||||||
@click.prevent="$utils.confirm(null,
|
@click.prevent="$utils.confirm(null,
|
||||||
() => changeCampaignStatus(props.row, 'paused'))">
|
() => changeCampaignStatus(props.row, 'paused'))" data-cy="btn-pause">
|
||||||
<b-tooltip :label="$t('campaigns.pause')" type="is-dark">
|
<b-tooltip :label="$t('campaigns.pause')" type="is-dark">
|
||||||
<b-icon icon="pause-circle-outline" size="is-small" />
|
<b-icon icon="pause-circle-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" v-if="canResume(props.row)"
|
<a href="" v-if="canResume(props.row)"
|
||||||
@click.prevent="$utils.confirm(null,
|
@click.prevent="$utils.confirm(null,
|
||||||
() => changeCampaignStatus(props.row, 'running'))">
|
() => changeCampaignStatus(props.row, 'running'))" data-cy="btn-resume">
|
||||||
<b-tooltip :label="$t('campaigns.send')" type="is-dark">
|
<b-tooltip :label="$t('campaigns.send')" type="is-dark">
|
||||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" v-if="canSchedule(props.row)"
|
<a href="" v-if="canSchedule(props.row)"
|
||||||
@click.prevent="$utils.confirm($t('campaigns.confirmSchedule'),
|
@click.prevent="$utils.confirm($t('campaigns.confirmSchedule'),
|
||||||
() => changeCampaignStatus(props.row, 'scheduled'))">
|
() => changeCampaignStatus(props.row, 'scheduled'))" data-cy="btn-schedule">
|
||||||
<b-tooltip :label="$t('campaigns.schedule')" type="is-dark">
|
<b-tooltip :label="$t('campaigns.schedule')" type="is-dark">
|
||||||
<b-icon icon="clock-start" size="is-small" />
|
<b-icon icon="clock-start" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" @click.prevent="previewCampaign(props.row)">
|
<a href="" @click.prevent="previewCampaign(props.row)" data-cy="btn-preview">
|
||||||
<b-tooltip :label="$t('campaigns.preview')" type="is-dark">
|
<b-tooltip :label="$t('campaigns.preview')" type="is-dark">
|
||||||
<b-icon icon="file-find-outline" size="is-small" />
|
<b-icon icon="file-find-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
|
@ -170,20 +174,22 @@
|
||||||
<a href="" @click.prevent="$utils.prompt($t('globals.buttons.clone'),
|
<a href="" @click.prevent="$utils.prompt($t('globals.buttons.clone'),
|
||||||
{ placeholder: $t('globals.fields.name'),
|
{ placeholder: $t('globals.fields.name'),
|
||||||
value: $t('campaigns.copyOf', { name: props.row.name }) },
|
value: $t('campaigns.copyOf', { name: props.row.name }) },
|
||||||
(name) => cloneCampaign(name, props.row))">
|
(name) => cloneCampaign(name, props.row))"
|
||||||
|
data-cy="btn-clone">
|
||||||
<b-tooltip :label="$t('globals.buttons.clone')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.clone')" type="is-dark">
|
||||||
<b-icon icon="file-multiple-outline" size="is-small" />
|
<b-icon icon="file-multiple-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" v-if="canCancel(props.row)"
|
<a href="" v-if="canCancel(props.row)"
|
||||||
@click.prevent="$utils.confirm(null,
|
@click.prevent="$utils.confirm(null,
|
||||||
() => changeCampaignStatus(props.row, 'cancelled'))">
|
() => changeCampaignStatus(props.row, 'cancelled'))"
|
||||||
|
data-cy="btn-cancel">
|
||||||
<b-tooltip :label="$t('globals.buttons.cancel')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.cancel')" type="is-dark">
|
||||||
<b-icon icon="cancel" size="is-small" />
|
<b-icon icon="cancel" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" @click.prevent="$utils.confirm($tc('campaigns.confirmDelete'),
|
<a href="" @click.prevent="$utils.confirm($tc('campaigns.confirmDelete'),
|
||||||
() => deleteCampaign(props.row))">
|
() => deleteCampaign(props.row))" data-cy="btn-delete">
|
||||||
<b-icon icon="trash-can-outline" size="is-small" />
|
<b-icon icon="trash-can-outline" size="is-small" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
<div class="tile is-parent is-vertical relative">
|
<div class="tile is-parent is-vertical relative">
|
||||||
<b-loading v-if="isCountsLoading" active :is-full-page="false" />
|
<b-loading v-if="isCountsLoading" active :is-full-page="false" />
|
||||||
<article class="tile is-child notification">
|
<article class="tile is-child notification" data-cy="lists">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<p class="title">{{ $utils.niceNumber(counts.lists.total) }}</p>
|
<p class="title">{{ $utils.niceNumber(counts.lists.total) }}</p>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
</div>
|
</div>
|
||||||
</article><!-- lists -->
|
</article><!-- lists -->
|
||||||
|
|
||||||
<article class="tile is-child notification">
|
<article class="tile is-child notification" data-cy="campaigns">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<p class="title">{{ $utils.niceNumber(counts.campaigns.total) }}</p>
|
<p class="title">{{ $utils.niceNumber(counts.campaigns.total) }}</p>
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
|
|
||||||
<div class="tile is-parent relative">
|
<div class="tile is-parent relative">
|
||||||
<b-loading v-if="isCountsLoading" active :is-full-page="false" />
|
<b-loading v-if="isCountsLoading" active :is-full-page="false" />
|
||||||
<article class="tile is-child notification">
|
<article class="tile is-child notification" data-cy="subscribers">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<p class="title">{{ $utils.niceNumber(counts.subscribers.total) }}</p>
|
<p class="title">{{ $utils.niceNumber(counts.subscribers.total) }}</p>
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
</div><!-- subscriber breakdown -->
|
</div><!-- subscriber breakdown -->
|
||||||
</div><!-- subscriber columns -->
|
</div><!-- subscriber columns -->
|
||||||
<hr />
|
<hr />
|
||||||
<div class="columns">
|
<div class="columns" data-cy="messages">
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<p class="title">{{ $utils.niceNumber(counts.messages) }}</p>
|
<p class="title">{{ $utils.niceNumber(counts.messages) }}</p>
|
||||||
<p class="is-size-6 has-text-grey">
|
<p class="is-size-6 has-text-grey">
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<p>{{ $t('forms.selectHelp') }}</p>
|
<p>{{ $t('forms.selectHelp') }}</p>
|
||||||
|
|
||||||
<b-loading :active="loading.lists" :is-full-page="false" />
|
<b-loading :active="loading.lists" :is-full-page="false" />
|
||||||
<ul class="no">
|
<ul class="no" data-cy="lists">
|
||||||
<li v-for="l in publicLists" :key="l.id">
|
<li v-for="l in publicLists" :key="l.id">
|
||||||
<b-checkbox v-model="checked"
|
<b-checkbox v-model="checked"
|
||||||
:native-value="l.uuid">{{ l.name }}</b-checkbox>
|
:native-value="l.uuid">{{ l.name }}</b-checkbox>
|
||||||
|
@ -27,11 +27,11 @@
|
||||||
<h4>{{ $t('forms.publicSubPage') }}</h4>
|
<h4>{{ $t('forms.publicSubPage') }}</h4>
|
||||||
<p>
|
<p>
|
||||||
<a :href="`${settings['app.root_url']}/subscription/form`"
|
<a :href="`${settings['app.root_url']}/subscription/form`"
|
||||||
target="_blank">{{ settings['app.root_url'] }}/subscription/form</a>
|
target="_blank" data-cy="url">{{ settings['app.root_url'] }}/subscription/form</a>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column" data-cy="form">
|
||||||
<h4>{{ $t('forms.formHTML') }}</h4>
|
<h4>{{ $t('forms.formHTML') }}</h4>
|
||||||
<p>
|
<p>
|
||||||
{{ $t('forms.formHTMLHelp') }}
|
{{ $t('forms.formHTMLHelp') }}
|
||||||
|
|
|
@ -11,9 +11,11 @@
|
||||||
<b-field :label="$t('import.mode')">
|
<b-field :label="$t('import.mode')">
|
||||||
<div>
|
<div>
|
||||||
<b-radio v-model="form.mode" name="mode"
|
<b-radio v-model="form.mode" name="mode"
|
||||||
native-value="subscribe">{{ $t('import.subscribe') }}</b-radio>
|
native-value="subscribe"
|
||||||
|
data-cy="check-subscribe">{{ $t('import.subscribe') }}</b-radio>
|
||||||
<b-radio v-model="form.mode" name="mode"
|
<b-radio v-model="form.mode" name="mode"
|
||||||
native-value="blocklist">{{ $t('import.blocklist') }}</b-radio>
|
native-value="blocklist"
|
||||||
|
data-cy="check-blocklist">{{ $t('import.blocklist') }}</b-radio>
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,13 +12,13 @@
|
||||||
</header>
|
</header>
|
||||||
<section expanded class="modal-card-body">
|
<section expanded class="modal-card-body">
|
||||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" name="name"
|
||||||
:placeholder="$t('globals.fields.name')" required></b-input>
|
:placeholder="$t('globals.fields.name')" required></b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('lists.type')" label-position="on-border"
|
<b-field :label="$t('lists.type')" label-position="on-border"
|
||||||
:message="$t('lists.typeHelp')">
|
:message="$t('lists.typeHelp')">
|
||||||
<b-select v-model="form.type" :placeholder="$t('lists.typeHelp')" required>
|
<b-select v-model="form.type" name="type" :placeholder="$t('lists.typeHelp')" required>
|
||||||
<option value="private">{{ $t('lists.types.private') }}</option>
|
<option value="private">{{ $t('lists.types.private') }}</option>
|
||||||
<option value="public">{{ $t('lists.types.public') }}</option>
|
<option value="public">{{ $t('lists.types.public') }}</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
|
@ -26,14 +26,14 @@
|
||||||
|
|
||||||
<b-field :label="$t('lists.optin')" label-position="on-border"
|
<b-field :label="$t('lists.optin')" label-position="on-border"
|
||||||
:message="$t('lists.optinHelp')">
|
:message="$t('lists.optinHelp')">
|
||||||
<b-select v-model="form.optin" placeholder="Opt-in type" required>
|
<b-select v-model="form.optin" name="optin" placeholder="Opt-in type" required>
|
||||||
<option value="single">{{ $t('lists.optins.single') }}</option>
|
<option value="single">{{ $t('lists.optins.single') }}</option>
|
||||||
<option value="double">{{ $t('lists.optins.double') }}</option>
|
<option value="double">{{ $t('lists.optins.double') }}</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('globals.terms.tags')" label-position="on-border">
|
<b-field :label="$t('globals.terms.tags')" label-position="on-border">
|
||||||
<b-taginput v-model="form.tags" ellipsis
|
<b-taginput v-model="form.tags" name="tags" ellipsis
|
||||||
icon="tag-outline" :placeholder="$t('globals.terms.tags')"></b-taginput>
|
icon="tag-outline" :placeholder="$t('globals.terms.tags')"></b-taginput>
|
||||||
</b-field>
|
</b-field>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="column has-text-right">
|
<div class="column has-text-right">
|
||||||
<b-button type="is-primary" icon-left="plus" @click="showNewForm">
|
<b-button type="is-primary" icon-left="plus" @click="showNewForm" data-cy="btn-new">
|
||||||
{{ $t('globals.buttons.new') }}
|
{{ $t('globals.buttons.new') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
backend-sorting @sort="onSort"
|
backend-sorting @sort="onSort"
|
||||||
>
|
>
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<b-table-column field="name" :label="$t('globals.fields.name')"
|
<b-table-column field="name" :label="$t('globals.fields.name')" header-class="cy-name"
|
||||||
sortable width="25%" paginated backend-pagination pagination-position="both"
|
sortable width="25%" paginated backend-pagination pagination-position="both"
|
||||||
@page-change="onPageChange">
|
@page-change="onPageChange" :data-id="props.row.id">
|
||||||
<div>
|
<div>
|
||||||
<router-link :to="{name: 'subscribers_list', params: { listID: props.row.id }}">
|
<router-link :to="{name: 'subscribers_list', params: { listID: props.row.id }}">
|
||||||
{{ props.row.name }}
|
{{ props.row.name }}
|
||||||
|
@ -36,20 +36,22 @@
|
||||||
</div>
|
</div>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="type" :label="$t('globals.fields.type')" sortable>
|
<b-table-column field="type" :label="$t('globals.fields.type')" header-class="cy-type"
|
||||||
|
sortable>
|
||||||
<div>
|
<div>
|
||||||
<b-tag :class="props.row.type">
|
<b-tag :class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||||
{{ $t('lists.types.' + props.row.type) }}
|
{{ $t('lists.types.' + props.row.type) }}
|
||||||
</b-tag>
|
</b-tag>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<b-tag>
|
<b-tag :data-cy="`optin-${props.row.optin}`">
|
||||||
<b-icon :icon="props.row.optin === 'double' ?
|
<b-icon :icon="props.row.optin === 'double' ?
|
||||||
'account-check-outline' : 'account-off-outline'" size="is-small" />
|
'account-check-outline' : 'account-off-outline'" size="is-small" />
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
{{ $t('lists.optins.' + props.row.optin) }}
|
{{ $t('lists.optins.' + props.row.optin) }}
|
||||||
</b-tag>{{ ' ' }}
|
</b-tag>{{ ' ' }}
|
||||||
<a v-if="props.row.optin === 'double'" class="is-size-7 send-optin"
|
<a v-if="props.row.optin === 'double'" class="is-size-7 send-optin"
|
||||||
href="#" @click="$utils.confirm(null, () => createOptinCampaign(props.row))">
|
href="#" @click="$utils.confirm(null, () => createOptinCampaign(props.row))"
|
||||||
|
data-cy="btn-send-optin-campaign">
|
||||||
<b-tooltip :label="$t('lists.sendOptinCampaign')" type="is-dark">
|
<b-tooltip :label="$t('lists.sendOptinCampaign')" type="is-dark">
|
||||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||||
{{ $t('lists.sendOptinCampaign') }}
|
{{ $t('lists.sendOptinCampaign') }}
|
||||||
|
@ -58,33 +60,35 @@
|
||||||
</div>
|
</div>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="subscriber_count" :label="$t('globals.terms.lists')"
|
<b-table-column field="subscriber_count" :label="$t('globals.terms.subscribers')"
|
||||||
numeric sortable centered>
|
header-class="cy-subscribers" numeric sortable centered>
|
||||||
<router-link :to="`/subscribers/lists/${props.row.id}`">
|
<router-link :to="`/subscribers/lists/${props.row.id}`">
|
||||||
{{ props.row.subscriberCount }}
|
{{ props.row.subscriberCount }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="created_at" :label="$t('globals.fields.createdAt')" sortable>
|
<b-table-column field="created_at" :label="$t('globals.fields.createdAt')"
|
||||||
|
header-class="cy-created_at" sortable>
|
||||||
{{ $utils.niceDate(props.row.createdAt) }}
|
{{ $utils.niceDate(props.row.createdAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
<b-table-column field="updated_at" :label="$t('globals.fields.updatedAt')" sortable>
|
<b-table-column field="updated_at" :label="$t('globals.fields.updatedAt')"
|
||||||
|
header-class="cy-updated_at" sortable>
|
||||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column class="actions" align="right">
|
<b-table-column class="actions" align="right">
|
||||||
<div>
|
<div>
|
||||||
<router-link :to="`/campaigns/new?list_id=${props.row.id}`">
|
<router-link :to="`/campaigns/new?list_id=${props.row.id}`" data-cy="btn-campaign">
|
||||||
<b-tooltip :label="$t('lists.sendCampaign')" type="is-dark">
|
<b-tooltip :label="$t('lists.sendCampaign')" type="is-dark">
|
||||||
<b-icon icon="rocket-launch-outline" size="is-small" />
|
<b-icon icon="rocket-launch-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a href="" @click.prevent="showEditForm(props.row)">
|
<a href="" @click.prevent="showEditForm(props.row)" data-cy="btn-edit">
|
||||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||||
<b-icon icon="pencil-outline" size="is-small" />
|
<b-icon icon="pencil-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" @click.prevent="deleteList(props.row)">
|
<a href="" @click.prevent="deleteList(props.row)" data-cy="btn-delete">
|
||||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||||
<b-icon icon="trash-can-outline" size="is-small" />
|
<b-icon icon="trash-can-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
<div class="column has-text-right">
|
<div class="column has-text-right">
|
||||||
<b-button :disabled="!hasFormChanged"
|
<b-button :disabled="!hasFormChanged"
|
||||||
type="is-primary" icon-left="content-save-outline"
|
type="is-primary" icon-left="content-save-outline"
|
||||||
@click="onSubmit" class="isSaveEnabled">{{ $t('globals.buttons.save') }}</b-button>
|
@click="onSubmit" class="isSaveEnabled" data-cy="btn-save">
|
||||||
|
{{ $t('globals.buttons.save') }}
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -278,11 +280,11 @@
|
||||||
<div class="column is-2">
|
<div class="column is-2">
|
||||||
<b-field :label="$t('globals.buttons.enabled')">
|
<b-field :label="$t('globals.buttons.enabled')">
|
||||||
<b-switch v-model="item.enabled" name="enabled"
|
<b-switch v-model="item.enabled" name="enabled"
|
||||||
:native-value="true" />
|
:native-value="true" data-cy="btn-enable-smtp" />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field v-if="form.smtp.length > 1">
|
<b-field v-if="form.smtp.length > 1">
|
||||||
<a @click.prevent="$utils.confirm(null, () => removeSMTP(n))"
|
<a @click.prevent="$utils.confirm(null, () => removeSMTP(n))"
|
||||||
href="#" class="is-size-7">
|
href="#" class="is-size-7" data-cy="btn-delete-smtp">
|
||||||
<b-icon icon="trash-can-outline" size="is-small" />
|
<b-icon icon="trash-can-outline" size="is-small" />
|
||||||
{{ $t('globals.buttons.delete') }}
|
{{ $t('globals.buttons.delete') }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -8,16 +8,19 @@
|
||||||
<section expanded class="modal-card-body">
|
<section expanded class="modal-card-body">
|
||||||
<b-field label="Action">
|
<b-field label="Action">
|
||||||
<div>
|
<div>
|
||||||
<b-radio v-model="form.action" name="action" native-value="add">
|
<b-radio v-model="form.action" name="action" native-value="add"
|
||||||
|
data-cy="check-list-add">
|
||||||
{{ $t('globals.buttons.add') }}
|
{{ $t('globals.buttons.add') }}
|
||||||
</b-radio>
|
</b-radio>
|
||||||
<b-radio v-model="form.action" name="action" native-value="remove">
|
<b-radio v-model="form.action" name="action" native-value="remove"
|
||||||
|
data-cy="check-list-remove">
|
||||||
{{ $t('globals.buttons.remove') }}
|
{{ $t('globals.buttons.remove') }}
|
||||||
</b-radio>
|
</b-radio>
|
||||||
<b-radio
|
<b-radio
|
||||||
v-model="form.action"
|
v-model="form.action"
|
||||||
name="action"
|
name="action"
|
||||||
native-value="unsubscribe"
|
native-value="unsubscribe"
|
||||||
|
data-cy="check-list-unsubscribe"
|
||||||
>{{ $t('subscribers.markUnsubscribed') }}</b-radio>
|
>{{ $t('subscribers.markUnsubscribed') }}</b-radio>
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
|
@ -8,24 +8,25 @@
|
||||||
<h4 v-else>{{ $t('subscribers.newSubscriber') }}</h4>
|
<h4 v-else>{{ $t('subscribers.newSubscriber') }}</h4>
|
||||||
|
|
||||||
<p v-if="isEditing" class="has-text-grey is-size-7">
|
<p v-if="isEditing" class="has-text-grey is-size-7">
|
||||||
{{ $t('globals.fields.id') }}: {{ data.id }} /
|
{{ $t('globals.fields.id') }}: <span data-cy="id">{{ data.id }}</span> /
|
||||||
{{ $t('globals.fields.uuid') }}: {{ data.uuid }}
|
{{ $t('globals.fields.uuid') }}: {{ data.uuid }}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<section expanded class="modal-card-body">
|
<section expanded class="modal-card-body">
|
||||||
<b-field :label="$t('subscribers.email')" label-position="on-border">
|
<b-field :label="$t('subscribers.email')" label-position="on-border">
|
||||||
<b-input :maxlength="200" v-model="form.email" :ref="'focus'"
|
<b-input :maxlength="200" v-model="form.email" name="email" :ref="'focus'"
|
||||||
:placeholder="$t('subscribers.email')" required></b-input>
|
:placeholder="$t('subscribers.email')" required></b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||||
<b-input :maxlength="200" v-model="form.name"
|
<b-input :maxlength="200" v-model="form.name" name="name"
|
||||||
:placeholder="$t('globals.fields.name')"></b-input>
|
:placeholder="$t('globals.fields.name')"></b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('globals.fields.status')" label-position="on-border"
|
<b-field :label="$t('globals.fields.status')" label-position="on-border"
|
||||||
:message="$t('subscribers.blocklistedHelp')">
|
:message="$t('subscribers.blocklistedHelp')">
|
||||||
<b-select v-model="form.status" :placeholder="$t('globals.fields.status')" required>
|
<b-select v-model="form.status" name="status" :placeholder="$t('globals.fields.status')"
|
||||||
|
required>
|
||||||
<option value="enabled">{{ $t('subscribers.status.enabled') }}</option>
|
<option value="enabled">{{ $t('subscribers.status.enabled') }}</option>
|
||||||
<option value="blocklisted">{{ $t('subscribers.status.blocklisted') }}</option>
|
<option value="blocklisted">{{ $t('subscribers.status.blocklisted') }}</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
|
@ -42,7 +43,7 @@
|
||||||
|
|
||||||
<b-field :label="$t('subscribers.attribs')" label-position="on-border"
|
<b-field :label="$t('subscribers.attribs')" label-position="on-border"
|
||||||
:message="$t('subscribers.attribsHelp') + ' ' + egAttribs">
|
:message="$t('subscribers.attribsHelp') + ' ' + egAttribs">
|
||||||
<b-input v-model="form.strAttribs" type="textarea" />
|
<b-input v-model="form.strAttribs" name="attribs" type="textarea" />
|
||||||
</b-field>
|
</b-field>
|
||||||
<a href="https://listmonk.app/docs/concepts"
|
<a href="https://listmonk.app/docs/concepts"
|
||||||
target="_blank" rel="noopener noreferrer" class="is-size-7">
|
target="_blank" rel="noopener noreferrer" class="is-size-7">
|
||||||
|
|
|
@ -3,14 +3,16 @@
|
||||||
<header class="columns">
|
<header class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<h1 class="title is-4">{{ $t('globals.terms.subscribers') }}
|
<h1 class="title is-4">{{ $t('globals.terms.subscribers') }}
|
||||||
<span v-if="!isNaN(subscribers.total)">({{ subscribers.total }})</span>
|
<span v-if="!isNaN(subscribers.total)">
|
||||||
|
(<span data-cy="count">{{ subscribers.total }}</span>)
|
||||||
|
</span>
|
||||||
<span v-if="currentList">
|
<span v-if="currentList">
|
||||||
» {{ currentList.name }}
|
» {{ currentList.name }}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="column has-text-right">
|
<div class="column has-text-right">
|
||||||
<b-button type="is-primary" icon-left="plus" @click="showNewForm">
|
<b-button type="is-primary" icon-left="plus" @click="showNewForm" data-cy="btn-new">
|
||||||
{{ $t('globals.buttons.new') }}
|
{{ $t('globals.buttons.new') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,13 +25,13 @@
|
||||||
<b-field grouped>
|
<b-field grouped>
|
||||||
<b-input @input="onSimpleQueryInput" v-model="queryInput"
|
<b-input @input="onSimpleQueryInput" v-model="queryInput"
|
||||||
:placeholder="$t('subscribers.queryPlaceholder')" icon="magnify" ref="query"
|
:placeholder="$t('subscribers.queryPlaceholder')" icon="magnify" ref="query"
|
||||||
:disabled="isSearchAdvanced"></b-input>
|
:disabled="isSearchAdvanced" data-cy="search"></b-input>
|
||||||
<b-button native-type="submit" type="is-primary" icon-left="magnify"
|
<b-button native-type="submit" type="is-primary" icon-left="magnify"
|
||||||
:disabled="isSearchAdvanced"></b-button>
|
:disabled="isSearchAdvanced" data-cy="btn-search"></b-button>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="#" @click.prevent="toggleAdvancedSearch">
|
<a href="#" @click.prevent="toggleAdvancedSearch" data-cy="btn-advanced-search">
|
||||||
<b-icon icon="cog-outline" size="is-small" />
|
<b-icon icon="cog-outline" size="is-small" />
|
||||||
{{ $t('subscribers.advancedQuery') }}
|
{{ $t('subscribers.advancedQuery') }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -40,7 +42,8 @@
|
||||||
<b-input v-model="queryParams.queryExp"
|
<b-input v-model="queryParams.queryExp"
|
||||||
@keydown.native.enter="onAdvancedQueryEnter"
|
@keydown.native.enter="onAdvancedQueryEnter"
|
||||||
type="textarea" ref="queryExp"
|
type="textarea" ref="queryExp"
|
||||||
placeholder="subscribers.name LIKE '%user%' or subscribers.status='blocklisted'">
|
placeholder="subscribers.name LIKE '%user%' or subscribers.status='blocklisted'"
|
||||||
|
data-cy="query">
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field>
|
<b-field>
|
||||||
|
@ -55,8 +58,9 @@
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<b-button native-type="submit" type="is-primary"
|
<b-button native-type="submit" type="is-primary"
|
||||||
icon-left="magnify">{{ $t('subscribers.query') }}</b-button>
|
icon-left="magnify" data-cy="btn-query">{{ $t('subscribers.query') }}</b-button>
|
||||||
<b-button @click.prevent="toggleAdvancedSearch" icon-left="cancel">
|
<b-button @click.prevent="toggleAdvancedSearch" icon-left="cancel"
|
||||||
|
data-cy="btn-query-reset">
|
||||||
{{ $t('subscribers.reset') }}
|
{{ $t('subscribers.reset') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,15 +84,15 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<a href='' @click.prevent="showBulkListForm">
|
<a href='' @click.prevent="showBulkListForm" data-cy="btn-manage-lists">
|
||||||
<b-icon icon="format-list-bulleted-square" size="is-small" /> Manage lists
|
<b-icon icon="format-list-bulleted-square" size="is-small" /> Manage lists
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href='' @click.prevent="deleteSubscribers">
|
<a href='' @click.prevent="deleteSubscribers" data-cy="btn-delete-subscribers">
|
||||||
<b-icon icon="trash-can-outline" size="is-small" /> Delete
|
<b-icon icon="trash-can-outline" size="is-small" /> Delete
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href='' @click.prevent="blocklistSubscribers">
|
<a href='' @click.prevent="blocklistSubscribers" data-cy="btn-manage-blocklist">
|
||||||
<b-icon icon="account-off-outline" size="is-small" /> Blocklist
|
<b-icon icon="account-off-outline" size="is-small" /> Blocklist
|
||||||
</a>
|
</a>
|
||||||
</p><!-- selection actions //-->
|
</p><!-- selection actions //-->
|
||||||
|
@ -110,7 +114,8 @@
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<b-table-column field="status" :label="$t('globals.fields.status')" sortable>
|
<b-table-column field="status" :label="$t('globals.fields.status')"
|
||||||
|
header-class="cy-status" :data-id="props.row.id" sortable>
|
||||||
<a :href="`/subscribers/${props.row.id}`"
|
<a :href="`/subscribers/${props.row.id}`"
|
||||||
@click.prevent="showEditForm(props.row)">
|
@click.prevent="showEditForm(props.row)">
|
||||||
<b-tag :class="props.row.status">
|
<b-tag :class="props.row.status">
|
||||||
|
@ -119,7 +124,8 @@
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="email" :label="$t('subscribers.email')" sortable>
|
<b-table-column field="email" :label="$t('subscribers.email')"
|
||||||
|
header-class="cy-email" sortable>
|
||||||
<a :href="`/subscribers/${props.row.id}`"
|
<a :href="`/subscribers/${props.row.id}`"
|
||||||
@click.prevent="showEditForm(props.row)">
|
@click.prevent="showEditForm(props.row)">
|
||||||
{{ props.row.email }}
|
{{ props.row.email }}
|
||||||
|
@ -137,39 +143,43 @@
|
||||||
</b-taglist>
|
</b-taglist>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="name" :label="$t('globals.fields.name')" sortable>
|
<b-table-column field="name" :label="$t('globals.fields.name')"
|
||||||
|
header-class="cy-name" sortable>
|
||||||
<a :href="`/subscribers/${props.row.id}`"
|
<a :href="`/subscribers/${props.row.id}`"
|
||||||
@click.prevent="showEditForm(props.row)">
|
@click.prevent="showEditForm(props.row)">
|
||||||
{{ props.row.name }}
|
{{ props.row.name }}
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="lists" :label="$t('globals.terms.lists')" numeric centered>
|
<b-table-column field="lists" :label="$t('globals.terms.lists')"
|
||||||
|
header-class="cy-lists" numeric centered>
|
||||||
{{ listCount(props.row.lists) }}
|
{{ listCount(props.row.lists) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="created_at" :label="$t('globals.fields.createdAt')" sortable>
|
<b-table-column field="created_at" :label="$t('globals.fields.createdAt')"
|
||||||
|
header-class="cy-created_at" sortable>
|
||||||
{{ $utils.niceDate(props.row.createdAt) }}
|
{{ $utils.niceDate(props.row.createdAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="updated_at" :label="$t('globals.fields.updatedAt')" sortable>
|
<b-table-column field="updated_at" :label="$t('globals.fields.updatedAt')"
|
||||||
|
header-class="cy-updated_at" sortable>
|
||||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column class="actions" align="right">
|
<b-table-column class="actions" align="right">
|
||||||
<div>
|
<div>
|
||||||
<a :href="`/api/subscribers/${props.row.id}/export`">
|
<a :href="`/api/subscribers/${props.row.id}/export`" data-cy="btn-download">
|
||||||
<b-tooltip :label="$t('subscribers.downloadData')" type="is-dark">
|
<b-tooltip :label="$t('subscribers.downloadData')" type="is-dark">
|
||||||
<b-icon icon="cloud-download-outline" size="is-small" />
|
<b-icon icon="cloud-download-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a :href="`/subscribers/${props.row.id}`"
|
<a :href="`/subscribers/${props.row.id}`"
|
||||||
@click.prevent="showEditForm(props.row)">
|
@click.prevent="showEditForm(props.row)" data-cy="btn-edit">
|
||||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||||
<b-icon icon="pencil-outline" size="is-small" />
|
<b-icon icon="pencil-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href='' @click.prevent="deleteSubscriber(props.row)">
|
<a href='' @click.prevent="deleteSubscriber(props.row)" data-cy="btn-delete">
|
||||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||||
<b-icon icon="trash-can-outline" size="is-small" />
|
<b-icon icon="trash-can-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
|
@ -245,7 +255,7 @@ export default Vue.extend({
|
||||||
methods: {
|
methods: {
|
||||||
// Count the lists from which a subscriber has not unsubscribed.
|
// Count the lists from which a subscriber has not unsubscribed.
|
||||||
listCount(lists) {
|
listCount(lists) {
|
||||||
return lists.reduce((defVal, item) => (defVal + item.status !== 'unsubscribed' ? 1 : 0), 0);
|
return lists.reduce((defVal, item) => (defVal + (item.subscriptionStatus !== 'unsubscribed' ? 1 : 0)), 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleAdvancedSearch() {
|
toggleAdvancedSearch() {
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
</header>
|
</header>
|
||||||
<section expanded class="modal-card-body">
|
<section expanded class="modal-card-body">
|
||||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
<b-input :maxlength="200" :ref="'focus'" v-model="form.name" name="name"
|
||||||
:placeholder="$t('globals.fields.name')" required></b-input>
|
:placeholder="$t('globals.fields.name')" required />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('templates.rawHTML')" label-position="on-border">
|
<b-field :label="$t('templates.rawHTML')" label-position="on-border">
|
||||||
|
|
|
@ -32,31 +32,34 @@
|
||||||
|
|
||||||
<b-table-column class="actions" align="right">
|
<b-table-column class="actions" align="right">
|
||||||
<div>
|
<div>
|
||||||
<a href="#" @click.prevent="previewTemplate(props.row)">
|
<a href="#" @click.prevent="previewTemplate(props.row)" data-cy="btn-preview">
|
||||||
<b-tooltip :label="$t('templates.preview')" type="is-dark">
|
<b-tooltip :label="$t('templates.preview')" type="is-dark">
|
||||||
<b-icon icon="file-find-outline" size="is-small" />
|
<b-icon icon="file-find-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" @click.prevent="showEditForm(props.row)">
|
<a href="#" @click.prevent="showEditForm(props.row)" data-cy="btn-edit">
|
||||||
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.edit')" type="is-dark">
|
||||||
<b-icon icon="pencil-outline" size="is-small" />
|
<b-icon icon="pencil-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a href="" @click.prevent="$utils.prompt(`Clone template`,
|
<a href="" @click.prevent="$utils.prompt(`Clone template`,
|
||||||
{ placeholder: 'Name', value: `Copy of ${props.row.name}`},
|
{ placeholder: 'Name', value: `Copy of ${props.row.name}`},
|
||||||
(name) => cloneTemplate(name, props.row))">
|
(name) => cloneTemplate(name, props.row))"
|
||||||
|
data-cy="btn-clone">
|
||||||
<b-tooltip :label="$t('globals.buttons.clone')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.clone')" type="is-dark">
|
||||||
<b-icon icon="file-multiple-outline" size="is-small" />
|
<b-icon icon="file-multiple-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a v-if="!props.row.isDefault" href="#"
|
<a v-if="!props.row.isDefault" href="#"
|
||||||
@click.prevent="$utils.confirm(null, () => makeTemplateDefault(props.row))">
|
@click.prevent="$utils.confirm(null, () => makeTemplateDefault(props.row))"
|
||||||
|
data-cy="btn-set-default">
|
||||||
<b-tooltip :label="$t('templates.makeDefault')" type="is-dark">
|
<b-tooltip :label="$t('templates.makeDefault')" type="is-dark">
|
||||||
<b-icon icon="check-circle-outline" size="is-small" />
|
<b-icon icon="check-circle-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</a>
|
</a>
|
||||||
<a v-if="!props.row.isDefault"
|
<a v-if="!props.row.isDefault"
|
||||||
href="#" @click.prevent="$utils.confirm(null, () => deleteTemplate(props.row))">
|
href="#" @click.prevent="$utils.confirm(null, () => deleteTemplate(props.row))"
|
||||||
|
data-cy="btn-delete">
|
||||||
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
<b-tooltip :label="$t('globals.buttons.delete')" type="is-dark">
|
||||||
<b-icon icon="trash-can-outline" size="is-small" />
|
<b-icon icon="trash-can-outline" size="is-small" />
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue