...
 
Commits (10)
......@@ -48,12 +48,14 @@ qa:e2e:
- >
if [ "$CI_BUILD_REF_NAME" == "master" ]; then
export E2E_DOMAIN=https://www.minds.com
export PRODUCTION=true
else
export E2E_DOMAIN=https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
export PRODUCTION=false
fi
- export CYPRESS_baseUrl=$E2E_DOMAIN
- echo "E2E tests for $CI_BUILD_REF_NAME running against $E2E_DOMAIN with user $CYPRESS_username"
- $(npm bin)/cypress run --browser chrome --record --key $CYPRESS_RECORD_ID --config CYPRESS_baseUrl=$E2E_DOMAIN
- $(npm bin)/cypress run --browser chrome --record --key $CYPRESS_RECORD_ID --config CYPRESS_baseUrl=$E2E_DOMAIN,production=$PRODUCTION
artifacts:
when: always
paths:
......
......@@ -12,16 +12,19 @@ function usage()
local help="Usage: e2e.sh [OPTIONS]
Intended to serve as an interaction wrapper around Cypress.
Intended to serve as an interaction wrapper around Cypress. Ensure that you run from within the project.
Example: ./e2e.sh -u nemofin -p password123 -v true -url http://www.minds.com/
Example: ./e2e.sh -u nemofin -p password123 -v true -h http://www.minds.com/
Options (* indicates it is required):"
local help_options="
*\-p ,\--password \<Parameter>\ The password of the user.
\-url ,\--url \<Parameter>\ The URL of the host e.g. https://www.minds.com/ - defaults to use localhost.
\-h ,\--url \<Parameter>\ The URL of the host e.g. https://www.minds.com/ - defaults to use localhost.
\-u ,\--username \<Parameter>\ The username - defaults to cypress_e2e_test.
\-pu ,\--pro_username \<Parameter>\ The pro users username.
\-pp ,\--pro_password \<Parameter>\ The pro users password
\-v ,\---video \<Parameter>\ true if you want video providing.
\-e, \--env \<Parameter>\ add additional env variables e.g. production=true
"
if [ "$missing_required" != "" ]
......@@ -53,14 +56,16 @@ POSITIONAL=()
# set default arguments
url="http://localhost"
username="minds_cypress_tests"
pro_username="minds_pro_cypress_tests"
pro_password=""
env=""
_video=false
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
-url|--url)
-h|--url)
url="$2"
shift 2
;;
......@@ -72,10 +77,22 @@ case $key in
password="$2"
shift 2
;;
-v|---video)
-v|--video)
_video="$2"
shift 2
;;
-pu|--pro-username)
pro_username="$2"
shift 2
;;
-pp|--pro-password)
pro_password="$2"
shift 2
;;
-e|--env)
env=",$2"
shift 2
;;
*)
POSITIONAL+=("$1") # saves unknown option in array
shift
......@@ -103,4 +120,5 @@ init_args $@
while [[ $PWD != '/' && ${PWD##*/} != 'front' ]]; do cd ..; done
#run cypress with args.
./node_modules/cypress/bin/cypress open --config baseUrl=$url,video=$_video --env username=$username,password=$password
echo $(npm bin)/cypress open --config baseUrl=$url,video=$_video --env username=$username,password=$password,pro_username=$pro_username,pro_password=$pro_password$env $POSITIONAL
$(npm bin)/cypress open --config baseUrl=$url,video=$_video --env username=$username,password=$password,pro_username=$pro_username,pro_password=$pro_password$env $POSITIONAL
......@@ -2,24 +2,29 @@
context('Blogs', () => {
before(() => {
cy.clearCookies();
cy.getCookie('minds_sess').then(sessionCookie => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route('POST', '**/api/v1/blog/new').as('postBlog');
cy.route('POST', '**/api/v1/media**').as('postMedia');
cy.route('GET', '**/api/v1/blog/**').as('getBlog');
cy.route('DELETE', '**/api/v1/blog/**').as('deleteBlog');
cy.visit('/newsfeed/global/top')
.location('pathname')
.should('eq', '/newsfeed/global/top');
});
const uploadAvatar = () => {
cy.visit(`/${Cypress.env().username}`, { timeout: 30000 });
cy.visit(`/${Cypress.env().username}`);
cy.get('.m-channel--name .minds-button-edit button:first-child').click();
cy.uploadFile(
'.minds-avatar input[type=file]',
......@@ -30,7 +35,7 @@ context('Blogs', () => {
};
const createBlogPost = (title, body, nsfw = false, schedule = false) => {
cy.visit('/blog/edit/new', { timeout: 30000 });
cy.visit('/blog/edit/new');
cy.uploadFile(
'.minds-banner input[type=file]',
......@@ -41,12 +46,17 @@ context('Blogs', () => {
cy.get('minds-textarea .m-editor').type(title);
cy.get('m-inline-editor .medium-editor-element').type(body);
// click on plus button
cy.get('.medium-editor-element > .medium-insert-buttons > button.medium-insert-buttons-show').click();
// click on camera
cy.get('ul.medium-insert-buttons-addons > li > button.medium-insert-action:first-child').contains('photo_camera').click();
// // click on plus button
// cy.get('.medium-editor-element > .medium-insert-buttons > button.medium-insert-buttons-show').click();
// // click on camera
// cy.get('ul.medium-insert-buttons-addons > li > button.medium-insert-action:first-child').contains('photo_camera').click();
// upload the image
cy.uploadFile('.medium-media-file-input', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
cy.uploadFile('.medium-media-file-input', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg')
.wait('@postMedia').then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
// open license dropdown & select first license
cy.get('.m-license-info select').select('All rights reserved');
......@@ -108,23 +118,20 @@ context('Blogs', () => {
});
}
cy.get('.m-button--submit', { timeout: 5000 }).click({ force: true }); // TODO: Investigate why disabled flag is being detected
cy.wait('@postBlog').then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
cy.wait('@getBlog').then(xhr => {
cy.get('.m-button--submit')
.click({ force: true }) // TODO: Investigate why disabled flag is being detected
.wait('@postBlog').then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
})
.wait('@getBlog').then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
expect(xhr.response.body).to.have.property('blog');
});
});
cy.location('pathname', { timeout: 30000 }).should(
'contains',
`/${Cypress.env().username}/blog`
);
cy.location('pathname')
.should('contains', `/${Cypress.env().username}/blog`);
cy.get('.m-blog--title').contains(title);
cy.get('.minds-blog-body p').contains(body);
......@@ -191,14 +198,8 @@ context('Blogs', () => {
cy.get('.minds-blog-body p').contains(body);
};
it('should be able to create a new scheduled blog', () => {
uploadAvatar();
createBlogPost('Title', 'Content', true, true);
deleteBlogPost();
});
it('should not be able to create a new blog if no title or banner are specified', () => {
cy.visit('/blog/edit/new', { timeout: 30000 });
cy.visit('/blog/edit/new');
cy.get('.m-button--submit').click();
cy.get('.m-blog--edit--error').contains('Error: You must provide a title');
cy.get('minds-textarea .m-editor').type('Title');
......@@ -215,7 +216,21 @@ context('Blogs', () => {
deleteBlogPost();
});
it('should create an activity for the blog post', () => {
/**
* Skipping until the scheduling options are visible on sandboxes
* currently they are not, missing setting perhaps?
*/
it.skip('should be able to create a new scheduled blog', () => {
uploadAvatar();
createBlogPost('Title', 'Content', true, true);
deleteBlogPost();
});
/**
* Skipping until sandbox behaves consistently as currently when posting,
* on the sandbox it does not update the newsfeed and channel straight away as it does on prod.
*/
it.skip('should create an activity for the blog post', () => {
const identifier = Math.floor(Math.random() * 100);
const title = 'Test Post for Activity ' + identifier;
const body = 'Some content here ' + identifier;
......@@ -244,7 +259,11 @@ context('Blogs', () => {
);
});
it('should update the activity when blog is updated', () => {
/**
* Skipping until sandbox behaves consistently as currently when posting,
* on the sandbox it does not update the newsfeed and channel straight away as it does on prod.
*/
it.skip('should update the activity when blog is updated', () => {
const identifier = Math.floor(Math.random() * 100);
const title = 'Test Post for Activity ' + identifier;
const body = 'Some content here ' + identifier;
......
......@@ -14,13 +14,13 @@ context('Boost Console', () => {
return cy.login(true);
}
});
newBoost(postContent, 100);
newBoost(postContent, 500);
});
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.preserveCookies();
cy.visit('/boost/console/newsfeed/history');
});
......
context('Boost Creation', () => {
const duplicateError = "There's already an ongoing boost for this entity";
const postContent = "Test boost, please reject..." + Math.random().toString(36).substring(8);
const nonParticipationError = 'Boost target should participate in the Rewards program.'
/**
* @author Ben Hayward
* @desc E2E testing for Minds Boost Console pages.
*/
import generateRandomId from '../../support/utilities';
beforeEach(() => {
cy.server()
.route("GET", '**/api/v2/boost/prepare/**').as('prepare')
.route("POST", '**/api/v2/boost/activity/**').as('activity')
.route("GET", '**/api/v2/blockchain/wallet/balance*').as('balance')
.route("GET", '**/api/v2/search/suggest/**').as('suggest');
context('Boost Console', () => {
const postContent = "Test boost, please reject..." + generateRandomId();
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
newBoost(postContent, 500);
});
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
beforeEach(() => {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.preserveCookies();
cy.visit('/boost/console/newsfeed/history');
});
after(() => {
cy.clearCookies();
});
it('should redirect a user to buy tokens when clicked', () => {
openTopModal();
it('should show a new boost in the console', () => {
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
cy.get('m-boost-console-card:nth-child(1) .m-mature-message span')
.contains(postContent);
});
cy.get('m-boost--creator-payment-methods li h5 span')
.contains('Buy Tokens')
it('should allow a revoke a boost', () => {
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
cy.get('m-boost-console-card:nth-child(1) .m-boost-card--manager-item--buttons > button')
.click();
cy.location('pathname', { timeout: 30000 })
.should('eq', `/token`);
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.contains('revoked');
});
it('should allow a user to make an offchain boost for 5000 tokens', () => {
cy.post(postContent);
openTopModal();
cy.get('.m-boost--creator-section-amount input')
.type(5000);
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
it('should load show the user content for newsfeed boosts', () => {
cy.route("GET", "**/feeds/container/*/activities**").as("activities");
cy.contains('Create a Boost')
.click()
.wait('@prepare').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
}).wait('@activity').then((xhr) => {
.location('pathname')
.should('eq', `/boost/console/newsfeed/create`)
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-overlay-modal')
.should('not.be.visible')
});
it('should error if the boost is a duplicate', () => {
openTopModal();
cy.get('.m-boost--creator-section-amount input')
.type(5000);
})
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
.click()
.wait('@prepare').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
}).wait('@activity').then((xhr) => {
it('should load show the user content for sidebar boosts', () => {
cy.route("GET", "**/api/v2/feeds/container/*/all**").as("all");
cy.visit('/boost/console/content/create')
.location('pathname')
.should('eq', `/boost/console/content/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("error");
});
cy.get('[data-cy=data-minds-boost-creation-error]')
.contains(duplicateError);
});
})
it('should display an error if boost offer receiver has not signed up for rewards', () => {
openTopModal();
cy.get('h4')
.contains('Offers')
.click();
cy.get('m-boost--creator-p2p-search .m-boost--creator-wide-input input')
.type("minds").wait('@suggest').then((xhr) => {
it('should load show the user content for offers', () => {
cy.route("GET", "**/api/v2/feeds/container/*/activities**").as("all");
cy.visit('/boost/console/offers/create')
.location('pathname')
.should('eq', `/boost/console/offers/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-boost--creator-autocomplete--results .m-boost--creator-autocomplete--result-content')
.first()
.click({force: true});
})
cy.get('[data-cy=data-minds-boost-creation-error]')
.contains(nonParticipationError);
});
function newBoost(text, views) {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.visit('/newsfeed/subscribed');
cy.post(text);
function openTopModal() {
cy.get('#boost-actions')
.first()
.click()
.wait('@balance').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
.click();
cy.get('.m-boost--creator-section-amount input')
.type(views);
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
.click();
cy.wait('@boostPost').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-overlay-modal')
.should('not.be.visible')
}
})
// Cannot test until env behaves consistently else,
// the test will frequently error when it cant see a boost.
context.skip('Boost Impressions', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
......@@ -27,9 +28,6 @@ context.skip('Boost Impressions', () => {
cy.reload();
})
// Cannot test until env behaves consistently.
// See: https://gitlab.com/minds/front/issues/1912
it('should register views on scroll', () => {
//smooth scroll
cy.scrollTo('0', '1%', { duration: 100 });
......
context('Channel image upload', () => {
/**
* Skipping until sandbox behaves consistently as currently when posting,
* on the sandboxes it does not show your latest image after you have posted it.
* The below code should be functioning correctly once this is resolved.
*/
context.skip('Channel image upload', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
return cy.login(true);
}
});
});
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', '/newsfeed/subscriptions');
});
beforeEach(()=> {
cy.preserveCookies();
cy.server();
......
context('Comment Permissions', () => {
// skipped until feat release
context.skip('Comment Permissions', () => {
const postMenu = 'minds-activity:first > div > m-post-menu';
const deletePostOption = "m-post-menu > ul > li:visible:contains('Delete')";
......@@ -87,4 +87,4 @@ context('Comment Permissions', () => {
cy.get('minds-activity:first')
.find("i:contains('chat_bubble')");
});
});
\ No newline at end of file
});
......@@ -6,7 +6,9 @@ context('Discovery', () => {
return cy.login(true);
}
});
cy.visit('/newsfeed/global/top');
cy.visit('/newsfeed/global/top')
.location('pathname')
.should('eq', '/newsfeed/global/top');
});
beforeEach(()=> {
......
......@@ -32,17 +32,21 @@ context('Groups', () => {
// click on hashtags dropdown
cy.get('m-hashtags-selector .m-dropdown--label-container').click();
// select #ART
cy.get('m-hashtags-selector m-dropdown m-form-tags-input > div > span').contains('#art').click();
cy.get('m-hashtags-selector m-dropdown m-form-tags-input > div > span').contains('art').click();
// type in another hashtag manually
cy.get('m-hashtags-selector m-form-tags-input input').type('hashtag{enter}').click();
// click away
cy.get('m-hashtags-selector .minds-bg-overlay').click();
cy.get('.m-groups-save > button').contains('Create').click();
cy.route("POST", "**/api/v1/groups/group/*/banner*").as("postBanner");
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
}).wait('@postBanner').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
cy.get('.m-groupInfo__name').contains('test');
......
......@@ -52,6 +52,7 @@ context('Messenger', () => {
after(() => {
cy.deleteUser(testUsername, testPassword);
cy.clearCookies({log: true})
});
it('should allow a new user to set a password and send a message', () => {
......
......@@ -14,6 +14,10 @@ context('Newsfeed', () => {
cy.route('POST', '**/api/v1/media').as('mediaPOST');
cy.route('POST', '**/api/v1/newsfeed/**').as('newsfeedEDIT');
cy.route('POST', '**/api/v1/media/**').as('mediaEDIT');
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', '/newsfeed/subscriptions');
});
const deleteActivityFromNewsfeed = () => {
......@@ -93,63 +97,6 @@ context('Newsfeed', () => {
cy.location('pathname', { timeout: 20000 }).should('contains', 'media');
};
it('editing media post propagates to activity', () => {
const identifier = Math.floor(Math.random() * 100);
const content = 'This is a post with an image ' + identifier;
newActivityContent(content);
attachImageToActivity();
postActivityAndAwaitResponse(200);
cy.get('.minds-list > minds-activity:first .message').contains(content);
navigateToMediaPageFromNewsfeed();
cy.get('.m-media-content--heading', { timeout: 10000 }).contains(content);
cy.get('.minds-button-edit').click();
const newContent = content + ' changed';
cy.get('minds-textarea .m-editor')
.clear()
.type(newContent);
cy.get('.m-button--submit').click();
cy.wait('@mediaEDIT').then(xhr => {
expect(xhr.status).to.equal(200);
});
navigateToNewsfeed();
cy.get('.minds-list > minds-activity:first .message').contains(newContent);
deleteActivityFromNewsfeed();
});
it('editing a media activity propagates to media post', () => {
const identifier = Math.floor(Math.random() * 100);
const content = 'This is a post with an image ' + identifier;
newActivityContent(content);
attachImageToActivity();
postActivityAndAwaitResponse(200);
cy.get('.minds-list > minds-activity:first .message').contains(content);
cy.get('.minds-list > minds-activity:first .item-image img').should(
'be.visible'
);
const newContent = content + ' changed';
editActivityContent(newContent);
cy.get('.minds-list > minds-activity:first .message').contains(content);
navigateToMediaPageFromNewsfeed();
cy.get('.m-media-content--heading', { timeout: 10000 }).contains(newContent);
navigateToNewsfeed();
deleteActivityFromNewsfeed();
});
it('should post an activity picking hashtags from the dropdown', () => {
newActivityContent('This is a post');
......@@ -199,6 +146,9 @@ context('Newsfeed', () => {
deleteActivityFromNewsfeed();
});
/**
* Commenting out until scheduling is enabled properly on sandboxes
*/
it('should be able to post an activity picking a scheduled date and the edit it', () => {
cy.get('minds-newsfeed-poster').then((poster) => {
if (poster.find('.m-poster-date-selector__input').length > 0) {
......@@ -494,4 +444,71 @@ context('Newsfeed', () => {
cy.location('pathname').should('eq', '/groups/create');
});
/**
* Skipping until sandbox behaves consistently as currently when posting,
* on the sandbox it does not update the newsfeed and channel straight away as it does on prod.
*/
it.skip('editing media post propagates to activity', () => {
const identifier = Math.floor(Math.random() * 100);
const content = 'This is a post with an image ' + identifier;
newActivityContent(content);
attachImageToActivity();
postActivityAndAwaitResponse(200);
cy.get('.minds-list > minds-activity:first .message').contains(content);
navigateToMediaPageFromNewsfeed();
cy.get('.m-media-content--heading', { timeout: 10000 }).contains(content);
cy.get('.minds-button-edit').click();
const newContent = content + ' changed';
cy.get('minds-textarea .m-editor')
.clear()
.type(newContent);
cy.get('.m-button--submit').click();
cy.wait('@mediaEDIT').then(xhr => {
expect(xhr.status).to.equal(200);
});
navigateToNewsfeed();
cy.get('.minds-list > minds-activity:first .message').contains(newContent);
deleteActivityFromNewsfeed();
});
/**
* Skipping until sandbox behaves consistently as currently when posting,
* on the sandbox it does not update the newsfeed and channel straight away as it does on prod.
*/
it.skip('editing a media activity propagates to media post', () => {
const identifier = Math.floor(Math.random() * 100);
const content = 'This is a post with an image ' + identifier;
newActivityContent(content);
attachImageToActivity();
postActivityAndAwaitResponse(200);
cy.contains(content);
cy.get('.minds-list > minds-activity:first .item-image img').should(
'be.visible'
);
const newContent = content + ' changed';
editActivityContent(newContent);
cy.contains(content);
navigateToMediaPageFromNewsfeed();
cy.get('.m-media-content--heading', { timeout: 10000 }).contains(newContent);
navigateToNewsfeed();
deleteActivityFromNewsfeed();
});
});
......@@ -4,31 +4,25 @@
*/
import generateRandomId from '../support/utilities';
// Skipped until notifications are fixed on sandboxes
// See https://gitlab.com/minds/engine/issues/732
context.skip('Notification', () => {
//secondary user for testing.
let username = '';
let password = '';
const username = generateRandomId();
const password = generateRandomId()+'X#';
const commentText = 'test comment';
const postText = 'test comment'
const commentText = generateRandomId();
const postText = generateRandomId();
const postCommentButton = 'm-comment__poster > div > div.minds-body > div > div > a.m-post-button';
const commentButton = 'minds-activity > div.tabs > minds-button-comment > a';
const commentInput = 'm-comment__poster minds-textarea > div';
const commentContent ='.m-comment__bubble > p';
const notificationBell = 'm-notifications--topbar-toggle > a > i';
const notification = 'minds-notification';
/**
* Before all, generate username and password, login as the new user and log out.
* Next login to env user, make a post, and log out.
*/
before(() => {
username = generateRandomId();
password = generateRandomId()+'X#';
cy.newUser(username, password);
cy.logout();
......@@ -42,7 +36,9 @@ context.skip('Notification', () => {
*/
after(() => {
cy.clearCookies();
cy.login(true, username, password);
cy.visit(`/${Cypress.env().username}`);
cy.deleteUser(username, password);
});
......@@ -52,6 +48,8 @@ context.skip('Notification', () => {
* then switch users and check for the notification.
*/
beforeEach(() => {
cy.route("GET", '**/api/v1/notifications/all**').as('notifications');
cy.clearCookies();
cy.login(false, username, password);
......@@ -73,13 +71,16 @@ context.skip('Notification', () => {
cy.login();
// Open their notifications
cy.get(notificationBell).click();
cy.get(notificationBell).click()
.wait('@notifications').then((xhr) => {
expect(xhr.status).to.equal(200);
});
/**
* Notifications not working on test env.
* TODO: Check for notification - follow it
* through and check it leads to the post with postText.
*/
cy.get(notification)
.first()
.click();
cy.contains(commentText);
});
})
......@@ -43,28 +43,22 @@ context('Onboarding', () => {
.contains(welcomeText);
});
it('should allow a user to register', () => {
//select topics
cy.get(getTopic(3)).click().should('have.class', 'selected')
cy.get(getTopic(4)).click().should('have.class', 'selected')
cy.get(getTopic(5)).click().should('have.class', 'selected')
it('should allow a user to run through onboarding modals', () => {
//select topics
cy.get(getTopic(3)).click().should('have.class', 'selected')
cy.get(getTopic(4)).click().should('have.class', 'selected')
cy.get(getTopic(5)).click().should('have.class', 'selected')
//click
cy.get(nextButton).click();
});
it('should skip over subscribed channels', () => {
//click
cy.get(nextButton).click();
//TODO: Skipped over for now as subscribed channels is not working on staging environment.
cy.get(nextButton).click();
});
it('should let a user change their display name and description', () => {
cy.get(nameField).clear().type(name);
cy.get(descriptionfield).type(description);
cy.get(nextButton).click();
});
it('should allow a user to select their country', () => {
//set dialcode
cy.get(countryDropdown).click();
cy.get(ukOption).click();
......
This diff is collapsed.
This diff is collapsed.
......@@ -19,6 +19,7 @@ context('Registration', () => {
const submitButton = 'minds-form-register .mdl-card__actions button';
beforeEach(() => {
cy.clearCookies();
cy.visit('/login');
cy.location('pathname').should('eq', '/login');
cy.server();
......@@ -30,6 +31,7 @@ context('Registration', () => {
cy.location('pathname').should('eq', '/login');
cy.login(false, username, password);
cy.deleteUser(username, password);
cy.clearCookies();
})
it('should allow a user to register', () => {
......
......@@ -14,11 +14,11 @@ context('Remind', () => {
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v2/newsfeed/remind/*").as("postRemind");
});
it('should allow a user to remind their post', () => {
cy.server();
cy.route("POST", "**/api/v2/newsfeed/remind/*").as("postRemind");
//post
cy.post("test!!");
......@@ -29,13 +29,14 @@ context('Remind', () => {
//fill out text box in modal
cy.get('.m-modal-remind-composer textarea')
.focus()
.clear()
.type(remindText);
//post remind.
cy.get('.m-modal-remind-composer-send i')
.click();
cy.wait('@postRemind').then((xhr) => {
.click()
.wait('@postRemind').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
......
......@@ -5,14 +5,25 @@ context('Subscription', () => {
const messageButton = 'm-messenger--channel-button > button';
const userDropdown = 'minds-button-user-dropdown > button';
beforeEach(()=> {
cy.login(true);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v1/subscribe/*").as("subscribe");
cy.route("DELETE", "**/api/v1/subscribe/*").as("unsubscribe");
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
cy.visit(`/${user}`);
})
cy.visit(`/${user}/`);
cy.location('pathname')
.should('eq', `/${user}/`);
});
it('should allow a user to subscribe to another', () => {
subscribe();
......@@ -20,16 +31,26 @@ context('Subscription', () => {
it('should allow a user to unsubscribe',() => {
unsubscribe();
})
});
function subscribe() {
cy.get(subscribeButton).click();
cy.get(messageButton).should('be.visible');
cy.get(subscribeButton)
.click()
.wait('@subscribe').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(messageButton).should('be.visible')
}
function unsubscribe() {
cy.get(userDropdown).click();
cy.contains('Unsubscribe').click();
cy.contains('Unsubscribe')
.click()
.wait('@unsubscribe').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(subscribeButton).should('be.visible');
}
......
......@@ -20,7 +20,7 @@ context('Topbar', () => {
.contains('View Channel')
.click();
cy.location('pathname').should('eq', `/${Cypress.env().username}`);
cy.location('pathname').should('eq', `/${Cypress.env().username}/`);
});
it('clicking on the dropdown on the right should allow to go to settings', () => {
......
......@@ -5,28 +5,34 @@
* @desc Spec tests for Wire transactions.
*/
context('Wire', () => {
import generateRandomId from "../support/utilities";
// Issue to re-enable https://gitlab.com/minds/front/issues/1846
context.skip('Wire', () => {
const receiver = {
username: generateRandomId(),
password: generateRandomId()+'F!',
}
const sendAmount = 5000;
const wireButton = 'm-wire-channel > div > button';
const sendButton = '.m-wire--creator-section--last > div > button';
const modal = 'm-overlay-modal > div.m-overlay-modal';
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.newUser(receiver.username, receiver.password);
cy.logout();
});
beforeEach(()=> {
cy.preserveCookies();
cy.login(true);
});
//TODO: Remove me when we get user to user wires working on the review environment
it.skip('should allow a user to send a wire to another user', () => {
// Visit users page.
cy.visit('/minds');
afterEach(() => {
// cy.login(true, receiver.username, receiver.password);
cy.visit(`/${Cypress.env().username}`);
// Click profile wire button
cy.get(wireButton).click();
......@@ -40,4 +46,19 @@ context('Wire', () => {
cy.get(modal).should('be.hidden');
});
it('should allow a user to send a wire to another user', () => {
// Visit users page.
cy.visit(`/${receiver.username}`);
// Click profile wire button
cy.get(wireButton).click();
cy.wait(2000);
// Click send button
cy.get(sendButton).click();
cy.wait(5000);
//Make sure modal is hidden after 5 seconds.
cy.get(modal).should('be.hidden');
});
})
......@@ -72,21 +72,23 @@ const poster = {
* @returns void
*/
Cypress.Commands.add('login', (canary = false, username, password) => {
cy.clearCookies();
cy.setCookie('staging', "1"); // Run in staging mode. Note: does not impact review sites
username = username ? username : Cypress.env().username;
password = password ? password : Cypress.env().password;
cy.setCookie('staging', "1"); // Run in stagin mode. Note: does not impact review sites
cy.visit('/login');
cy.server();
cy.route("POST", "/api/v1/authenticate").as("postLogin");
cy.get(loginForm.username).type(username);
cy.get(loginForm.password).type(password);
cy.get(loginForm.username).focus().type(username);
cy.get(loginForm.password).focus().type(password);
cy.get(loginForm.submit).click();
cy.wait('@postLogin').then((xhr) => {
cy.get(loginForm.submit)
.focus()
.click({force: true})
.wait('@postLogin').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
......@@ -97,8 +99,7 @@ Cypress.Commands.add('login', (canary = false, username, password) => {
* @returns void
*/
Cypress.Commands.add('logout', () => {
cy.get(nav.hamburgerMenu).click();
cy.get(nav.logoutButton).click();
cy.visit('/logout')
});
/**
......@@ -111,9 +112,8 @@ Cypress.Commands.add('logout', () => {
* @returns void
*/
Cypress.Commands.add('newUser', (username = '', password = '') => {
cy.visit('/login');
cy.location('pathname', { timeout: 30000 })
cy.visit('/login')
.location('pathname')
.should('eq', `/login`);
cy.server();
......@@ -146,7 +146,7 @@ Cypress.Commands.add('newUser', (username = '', password = '') => {
});
Cypress.Commands.add('preserveCookies', () => {
Cypress.Cookies.preserveOnce('staging', 'minds_sess', 'mwa', 'XSRF-TOKEN');
Cypress.Cookies.preserveOnce('staging', 'minds_sess', 'mwa', 'XSRF-TOKEN', 'staging-features');
});
/**
......@@ -216,6 +216,17 @@ Cypress.Commands.add('post', (message) => {
});
});
/**
* Sets the feature flag cookie.
* @param { Object } flags - JSON object containing flags to turn on
* e.g. { dark mode:false, es-feeds: true }
* @returns void
*/
// Cypress.Commands.add('overrideFeatureFlag', (flags) => {
// const base64 = Buffer.from(JSON.stringify(flags)).toString("base64");
// cy.setCookie('staging-features', base64);
// });
/**
* Converts base64 to blob format
* @param { string } b64Data - The base64 data.
......
This diff is collapsed.
export interface Category {
id: string;
label: string;
metrics?: string[]; // TODO: remove this
permissions?: string[];
}
export interface Response {
status: string;
dashboard: Dashboard;
}
export interface Dashboard {
category: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
}
export interface Filter {
id: string;
label: string;
options: Option[];
}
export interface Option {
id: string;
label: string;
available?: boolean;
selected?: boolean;
interval?: string;
comparison_interval?: number;
from_ts_ms?: number;
from_ts_iso?: string;
}
export interface Metric {
id: string;
label: string;
permissions: string[];
summary: Summary;
visualisation: Visualisation | null;
}
export interface Summary {
current_value: number;
comparison_value: number;
comparison_interval: number;
comparison_positive_inclination: boolean;
}
export interface Visualisation {
type: string;
segments: Array<Buckets>;
}
export interface Buckets {
buckets: Bucket[];
}
export interface Bucket {
key: number;
date: string;
value: number;
}
export interface Timespan {
id: string;
label: string;
interval: string;
comparison_interval: number;
from_ts_ms: number;
from_ts_iso: string;
}
export interface UserState {
category: string;
timespan: string;
timespans: Timespan[];
metric: string;
metrics: Metric[];
filter: string[];
filters: Filter[];
loading: boolean;
}
const categories: Array<any> = [
{
id: 'summary',
label: 'Summary',
permissions: ['admin', 'user'],
metrics: [],
},
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// metrics: [],
// },
{
id: 'traffic',
label: 'Traffic',
......@@ -24,48 +24,48 @@ const categories: Array<any> = [
permissions: ['admin', 'user'],
metrics: ['total', 'pageviews', 'active_referrals', 'customers'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
metrics: ['posts', 'votes', 'comments', 'reminds', 'subscribers', 'tags'],
},
// {
// id: 'engagement',
// label: 'Engagement',
// permissions: ['admin', 'user'],
// metrics: ['posts', 'votes', 'comments', 'reminds', 'subscribers', 'tags'],
// },
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
metrics: ['top_content', 'top_channels'],
},
{
id: 'referrers',
label: 'Referrers',
permissions: ['admin', 'user'],
metrics: ['top_referrers'],
},
{
id: 'plus',
label: 'Plus',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
},
{
id: 'pro',
label: 'Pro',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
},
{
id: 'boost',
label: 'Boost',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_tokens'],
},
{
id: 'nodes',
label: 'Nodes',
permissions: ['admin'],
metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// metrics: ['top_referrers'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_tokens'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// metrics: ['transactions', 'users', 'revenue_usd', 'revenue_tokens'],
// },
];
export default categories;
......@@ -135,9 +135,9 @@ export class AnalyticsChartComponent implements OnInit, OnDestroy {
type: 'line',
layer: 'below',
x0: this.segments[0].buckets[i].date.slice(0, 10),
y0: 0, // this should be graph y min
y0: 0, // this should be rendered graph y min
x1: this.segments[0].buckets[i].date.slice(0, 10),
y1: this.segments[0].buckets[i].value, // this should be graph y max
y1: this.segments[0].buckets[i].value, // should be rendered graph y max
line: {
color: this.getColor('m-transparent'),
width: 2,
......
<div
class="filterWrapper"
[ngClass]="{ expanded: expanded, isMobile: isMobile }"
[ngClass]="{
expanded: expanded,
isMobile: isMobile,
dropUp: dropUp
}"
(blur)="expanded = false"
>
<div class="filterHeader" (click)="expanded = !expanded">
<span class="filterLabel">{{ filter.label }}</span>
<span class="option option--selected">
<span>{{ selectedOption.label }}</span>
</span>
<i class="material-icons" *ngIf="!expanded"> keyboard_arrow_down</i>
<i class="material-icons" *ngIf="expanded">keyboard_arrow_up</i>
<div class="row">
<span class="filterLabel">{{ filter.label }}</span>
<span class="option option--selected">
{{ selectedOption.label }}
</span>
<i class="material-icons" *ngIf="!expanded"> keyboard_arrow_down</i>
<i class="material-icons" *ngIf="expanded">keyboard_arrow_up</i>
</div>
</div>
<div class="unselectedOptionsContainer">
<ng-container *ngFor="let option of filter.options">
<div
class="option"
class="option row"
(click)="updateFilter(option)"
[ngClass]="{
unavailable: option.available === false
......
......@@ -4,24 +4,38 @@ m-analytics__filter {
z-index: 2;
}
.filterHeader,
.unselectedOptionsContainer {
padding: 10px 8px 8px 10px;
}
.filterWrapper {
cursor: pointer;
border-radius: 3px;
// display: flex;
// flex-direction: column;
transition: border-color 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
@include m-theme() {
background-color: themed($m-white);
border: 1px solid themed($m-grey-100);
color: rgba(themed($m-grey-200), 0.7);
}
&.dropUp {
flex-direction: column-reverse;
}
&.expanded {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
@include m-theme() {
border-color: themed($m-blue);
box-shadow: 0px 1px 15px 0 rgba(themed($m-black), 0.1);
}
&:not(.dropUp) {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
.unselectedOptionsContainer {
display: block;
}
.option {
&:hover:not(.unavailable) {
@include m-theme() {
......@@ -30,9 +44,12 @@ m-analytics__filter {
}
}
}
.row {
display: flex;
}
}
.option {
padding: 4px 8px;
border-radius: 3px;
@include m-theme() {
background-color: themed($m-white);
}
......@@ -50,15 +67,14 @@ m-analytics__filter {
display: flex;
justify-content: space-between;
.filterLabel {
padding: 4px 0 4px 8px;
margin-right: 10px;
}
i {
flex-grow: 0;
margin: 4px 4px 0 0;
}
}
.option--selected {
border-radius: 3px;
margin-right: 8px;
@include m-theme() {
color: themed($m-grey-500);
}
......@@ -77,10 +93,8 @@ m-analytics__filter {
border-top: 1px solid themed($m-blue);
background-color: themed($m-white);
}
.option:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
.option {
padding: 5px 0;
}
}
......
......@@ -30,8 +30,8 @@ import isMobileOrTablet from '../../../../../helpers/is-mobile-or-tablet';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsFilterComponent implements OnInit, OnDestroy {
// TODO: extend Filter interface to allow additional fields (like for timespans?)
@Input() filter: Filter;
@Input() dropUp: boolean = false;
isMobile: boolean;
expanded = false;
......
// .m-sidebarMarkers__container,
// m-v2-topbar {
// display: none;
// }
m-analytics__menu {
// ----------------------------------------
// MOBILE
.isMobile {
.topbar {
z-index: 99999;
position: fixed;
top: 0;
left: 0;
width: 100%;
padding: 16px;
text-align: center;
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-800);
}
i {
display: block;
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
@include m-theme() {
background-color: themed($m-grey-100);
color: themed($m-grey-700);
}
}
.pageTitle {
font-size: 20px;
margin: 0;
}
}
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
// display: none;
background-color: transparent;
transition: background-color 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.expanded {
// display: block;
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
.sidebar {
z-index: 999999;
position: fixed;
top: 0;
bottom: 0;
left: -340px;
transition: left 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
padding: 0 24px;
width: 300px;
max-width: 70%;
@include m-theme() {
background-color: themed($m-white);
}
&.expanded {
left: 0;
}
.sidebarTitle {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
margin: 0;
}
i {
font-size: 20px;
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.profile {
display: flex;
text-decoration: none;
margin: 24px 0;
@include m-theme() {
color: themed($m-grey-800);
}
.avatar {
border-radius: 50%;
margin-right: 16px;
}
.details {
& > {
padding: 8px 0;
}
.name {
font-weight: bold;
}
.subscribers {
font-size: 16px;
@include m-theme() {
color: themed($m-grey-300);
}
}
}
}
}
}
// ----------------------------------------
padding: 16px 16px 16px 16px;
flex: 1 1 0px;
i {
display: none;
}
.catContainer {
cursor: pointer;
.cat {
padding: 6px 0;
a {
text-decoration: none;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-200);
}
}
a.selected,
&:hover a {
@include m-theme() {
color: themed($m-blue);
}
}
}
}
}
.m-sidebarMarkers__container,
m-v2-topbar {
display: none;
}
// .m-sidebarMarkers__container,
// m-v2-topbar {
// display: none;
// }
m-analytics__menu {
// ----------------------------------------
......
<section class="metricsSection" [ngClass]="{ isMobile: isMobile }">
<div class="overflowFade--left"></div>
<!-- <div class="overflowFade--left"></div>
<div class="overflowScrollButton--left">
<i class="material-icons">chevron_left</i>
</div>
</div> -->
<div class="metricsWrapper">
<div class="metricsContainer" *ngIf="metrics$ | async as metrics">
<div
......@@ -13,11 +13,12 @@
>
<div class="metricLabel">{{ metric.label }}</div>
<!-- TODO the "number" pipe should be from backend so it can dynamically handle diff decimals/currency formats -->
<div class="metricSummary">
<div class="metricSummary" *ngIf="metric.summary">
{{ metric.summary.current_value | number }}
</div>
<div
*ngIf="metric.summary"
class="metricDelta"
[ngClass]="{
goodChange: metric.hasChanged && metric.positiveTrend,
......@@ -31,8 +32,8 @@
</div>
</div>
</div>
<div class="overflowFade--right"></div>
<!-- <div class="overflowFade--right"></div>
<div class="overflowScrollButton--right">
<i class="material-icons">chevron_right</i>
</div>
</div> -->
</section>
......@@ -47,7 +47,6 @@ m-analytics__metrics {
&:hover {
@include m-theme() {
border: 1px solid themed($m-blue);
// box-shadow: 0px 0px 5px -3px rgba(themed($m-black-always), 0.5);
}
}
&.overflowScrollButton--right {
......@@ -67,6 +66,7 @@ m-analytics__metrics {
.metricsWrapper {
position: relative;
overflow: hidden;
width: 95%;
}
.metricsContainer {
scroll-snap-type: x mandatory;
......@@ -74,8 +74,8 @@ m-analytics__metrics {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
width: 95%;
padding: 0 16px;
// padding: 0 16px;
@include m-theme() {
box-shadow: 0 7px 15px -7px rgba(themed($m-black-always), 0.1);
}
......@@ -88,7 +88,7 @@ m-analytics__metrics {
flex: 0 0 auto;
width: 20%;
padding: 24px 20px 20px 20px;
font-size: 12px;
font-size: 14px;
box-sizing: border-box;
@include m-theme() {
color: themed($m-grey-200);
......@@ -98,17 +98,24 @@ m-analytics__metrics {
border-bottom: 8px solid themed($m-blue);
}
}
&:first-child {
margin-left: 16px;
}
&:last-child {
margin-right: 16px;
}
&:hover:not(.active) {
cursor: pointer;
@include m-theme() {
background-color: rgba(themed($m-blue-grey-300), 0.08);
border-bottom: 5px solid themed($m-blue);
border-bottom: 8px solid themed($m-blue);
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
}
.metricSummary {
font-size: 16px;
font-size: 17px;
margin-top: 8px;
@include m-theme() {
color: themed($m-grey-800);
}
......
......@@ -51,20 +51,22 @@ export class AnalyticsMetricsComponent implements OnInit, OnDestroy {
const metrics = _metrics.map(metric => ({ ...metric })); // Clone to avoid updating
for (let metric of metrics) {
const delta =
(metric.summary.current_value - metric.summary.comparison_value) /
metric.summary.comparison_value;
if (metric.summary) {
const delta =
(metric.summary.current_value - metric.summary.comparison_value) /
metric.summary.comparison_value;
metric['delta'] = delta;
metric['hasChanged'] = delta === 0 ? false : true;
metric['delta'] = delta;
metric['hasChanged'] = delta === 0 ? false : true;
if (
(delta > 0 && metric.summary.comparison_positive_inclination) ||
(delta < 0 && !metric.summary.comparison_positive_inclination)
) {
metric['positiveTrend'] = true;
} else {
metric['positiveTrend'] = false;
if (
(delta > 0 && metric.summary.comparison_positive_inclination) ||
(delta < 0 && !metric.summary.comparison_positive_inclination)
) {
metric['positiveTrend'] = true;
} else {
metric['positiveTrend'] = false;
}
}
}
......
......@@ -9,7 +9,7 @@
type="text"
id="search"
autocomplete="off"
placeholder="Search for a channel"
placeholder="Filter by channel"
#searchInput
/>
<!-- <label class="mdl-textfield__label" for="search">abc</label> -->
......
@import 'defaults';
m-analytics__search {
display: none;
width: 200px;
.mdl-textfield {
......
<!-- <div *ngIf="vm$ | async as vm">
<p>table goes here</p>
</div> -->
<div class="tableWrapper">
<div class="header row">
<div
*ngFor="let col of visualisation.columns; let first = first"
[ngClass]="{
first: first
}"
class="col"
>
{{ col.label }}
</div>
</div>
<div class="body">
<!-- TODO: make some sort of a map thing to make the entity fields have the same name for different entity types. Currently only made it to 'work' with fake image and fake blog but even those are streching it -->
<ng-container *ngFor="let row of reformattedBuckets">
<div class="row">
<div class="entity col">
<div class="entityTitle">
<a [routerLink]="'/' + row.entity.route">{{ row.entity.title }}</a
><i class="material-icons">open_in_new</i>
</div>
<div class="entityDetails">
<a [routerLink]="'/' + row.entity.ownerObj.guid">{{
row.entity.ownerObj.name
}}</a>
<span>{{ row.entity.subtype | titlecase }}</span>
<span>Published {{ row.entity.time_created * 1000 | date }}</span>
</div>
</div>
<ng-container *ngFor="let value of row.values">
<div class="col">{{ value }}</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
<!-- <ng-template #entityCol>
</ng-template> -->
.filterableChartWrapper {
padding: 0;
width: 100%;
}
.tableWrapper {
font-size: 13px;
font-weight: 400;
@include m-theme() {
color: themed($m-grey-800);
}
.row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 16px 48px;
&.header {
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
color: themed($m-grey-200);
}
.col {
flex: 1 1 0;
&.first {
flex: 4 1 0;
}
}
}
.col {
flex: 1 1 0;
&.entity {
flex: 4 1 0;
// @include m-theme() {
// border-right: 1px solid themed($m-grey-50);
// }
a {
font-weight: 400;
text-decoration: none;
}
.entityTitle {
display: flex;
align-items: center;
i {
font-size: 13px;
display: none;
}
a,
i {
@include m-theme() {
color: themed($m-grey-800);
}
}
}
.entityDetails {
display: flex;
justify-content: space-between;
@include m-theme() {
color: themed($m-grey-200);
}
a {
@include m-theme() {
color: themed($m-grey-200);
}
}
}
}
}
}
.body {
.row {
&:hover {
@include m-theme() {
background-color: rgba(themed($m-blue-grey-50), 0.5);
}
.entityTitle i {
display: inline-block;
}
}
}
}
}
import { Component, OnInit } from '@angular/core';
import {
Component,
OnInit,
OnDestroy,
HostListener,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
Buckets,
} from '../../dashboard.service';
@Component({
selector: 'm-analytics-table',
selector: 'm-analytics__table',
templateUrl: './table.component.html',
// changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsTableComponent implements OnInit {
constructor() {}
export class AnalyticsTableComponent implements OnInit, OnDestroy {
subscription: Subscription;
visualisation: Visualisation;
columns: Array<any>;
rows: Array<any>;
reformattedBuckets: Array<any> = [];
minds = window.Minds;
selectedMetric$ = this.analyticsService.metrics$.pipe(
map(metrics => {
console.log(
metrics,
metrics.find(metric => metric.visualisation !== null)
);
return metrics.find(metric => metric.visualisation !== null);
})
);
selectedMetric;
constructor(
private analyticsService: AnalyticsDashboardService // protected cd: ChangeDetectorRef
) {}
ngOnInit() {
this.subscription = this.selectedMetric$.subscribe(metric => {
this.selectedMetric = metric;
this.visualisation = metric.visualisation;
this.columns = metric.visualisation.columns.sort((a, b) =>
a.order > b.order ? 1 : -1
);
this.reformatBuckets();
});
}
reformatBuckets() {
this.visualisation.buckets.forEach(bucket => {
const reformattedBucket = {};
const reformattedValues = [];
this.columns.forEach((column, i) => {
if (i === 0) {
reformattedBucket['entity'] = bucket.values[column.id];
} else {
reformattedValues.push(bucket.values[column.id]);
}
});
reformattedBucket['values'] = reformattedValues;
this.reformattedBuckets.push(reformattedBucket);
});
// TODO: reformat diff entity objs so template fields match
}
ngOnInit() {}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
......@@ -19,22 +19,10 @@ import { MindsTitle } from '../../../services/ux/title';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import {
AnalyticsDashboardService,
Category,
Response,
Dashboard,
Filter,
Option,
Metric,
Summary,
Visualisation,
Bucket,
Timespan,
UserState,
} from './dashboard.service';
import categories from './categories.default';
import { AnalyticsDashboardService } from './dashboard.service';
import { Filter } from './../../../interfaces/dashboard';
// import categories from './categories.default';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
@Component({
......
This diff is collapsed.
<m-analytics__metrics></m-analytics__metrics>
<m-analytics__metrics
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__metrics>
<div class="filterableChartWrapper">
<m-analytics__chart></m-analytics__chart>
<m-analytics__chart
*ngIf="selectedMetric.visualisation.type === 'chart'"
></m-analytics__chart>
<m-analytics__table
*ngIf="selectedMetric.visualisation.type === 'table'"
></m-analytics__table>
<m-analytics__filters></m-analytics__filters>
</div>
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { AnalyticsDashboardService } from '../../dashboard.service';
@Component({
selector: 'm-analytics__layout--chart',
......@@ -6,7 +9,26 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnalyticsLayoutChartComponent implements OnInit {
constructor() {}
subscription: Subscription;
selectedMetric$ = this.analyticsService.metrics$.pipe(
map(metrics => {
console.log(
metrics,
metrics.find(metric => metric.visualisation !== null)
);
return metrics.find(metric => metric.visualisation !== null);
})
);
selectedMetric;
constructor(private analyticsService: AnalyticsDashboardService) {}
ngOnInit() {}
ngOnInit() {
this.subscription = this.selectedMetric$.subscribe(metric => {
this.selectedMetric = metric;
try {
} catch (err) {
console.log(err);
}
});
}
}
......@@ -103,6 +103,13 @@
>
Error: You must upload a banner
</h1>
<h1
class="m-blog--edit--error"
i18n="@@M__COMMON__ERROR_GATEWAY_TIMEOUT"
*ngSwitchCase="'error:gateway-timeout'"
>
Error: Gateway Time-out
</h1>
<h1 class="m-blog--edit--error" *ngSwitchDefault>
Error: {{error}}
</h1>
......
......@@ -282,6 +282,7 @@ export class BlogEdit {
);
})
.catch(e => {
this.error = e;
this.canSave = true;
this.inProgress = false;
});
......
......@@ -278,6 +278,7 @@
*ngIf="showProSettings"
class="m-btn m-link-btn m-btn--with-icon m-btn--slim"
[routerLink]="proSettingsRouterLink"
data-cy="data-minds-sidebar-admin-pro-button"
>
<i class="material-icons">business_center</i>
<span i18n>Pro</span>
......
......@@ -6,6 +6,8 @@ import {
Input,
OnDestroy,
OnInit,
Output,
EventEmitter,
ViewChild,
} from '@angular/core';
......@@ -18,6 +20,7 @@ import { DynamicHostDirective } from '../../../common/directives/dynamic-host.di
templateUrl: 'entity.component.html',
})
export class NewsfeedEntityComponent {
@Output() deleted = new EventEmitter<boolean>();
@ViewChild(DynamicHostDirective, { static: false })
host: DynamicHostDirective;
entity;
......@@ -65,4 +68,12 @@ export class NewsfeedEntityComponent {
componentRef.changeDetectorRef.detectChanges();
}
}
/**
* Sets entity to null and by extension hides it.
*/
delete(): void {
this.entity = null;
this.deleted.emit(true);
}
}
......@@ -69,7 +69,11 @@ export class Upload {
progress(100);
resolve(JSON.parse(this.response));
} else {
reject(this.response);
if (this.status === 504) {
reject('error:gateway-timeout');
} else {
reject(this.response);
}
}
};
xhr.onreadystatechange = function() {
......