...
 
Commits (2)
......@@ -3,32 +3,38 @@
* @desc E2E testing for Minds Pro's pages.
*/
context('Pro Page', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
if (Cypress.env().pro_password) {
// required to run tests against pro user only.
const topBar = '.m-proChannel__topbar';
let categories = [
{ label: 'Technology', tag: '#technology' },
{ label: 'Food', tag: '#food' },
{ label: 'News', tag: '#news' }
{ label: 'News', tag: '#news' },
];
let footerLinks = [
{ label: 'Minds', link: 'https://www.minds.com/' },
{ label: 'Careers', link: 'https://www.minds.com/careers' },
];
const proButton = 'data-minds-sidebar-admin-pro-button';
function resetSettings() {
cy.visit(`/pro/settings`);
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.route('POST', '**/api/v2/pro/settings').as('settings');
cy.get('#title').focus().clear().type('Title');
cy.get('#headline').focus().clear().type('This is a headline');
cy.get('#title')
.focus()
.clear()
.type('Title');
cy.get('#headline')
.focus()
.clear()
.type('This is a headline');
cy.contains('Hashtags')
.click();
cy.contains('Hashtags').click();
// remove all hashtags
removeInputs();
......@@ -37,8 +43,7 @@ context('Pro Page', () => {
let cat = categories[i];
addTag(cat.label, cat.tag, i);
}
cy.contains('Footer')
.click();
cy.contains('Footer').click();
cy.get('#footer_text')
.clear()
......@@ -54,30 +59,36 @@ context('Pro Page', () => {
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
.wait('@settings')
.then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
}
);
});
}
function removeInputs() {
cy.get('.m-draggableList__list .m-proSettings__field .m-proSettings__flexInputs').should('be.visible').within($el => {
for (let i = $el.length - 1; i >= 0; i--) { // flexInput. Start from the last one
let c = $el[i];
for (let j = 0; j < c.children.length; j++) { // inputs and the X button
let cc = c.children[j];
if (cc.nodeName === 'I') { // if it's the X button, click on it
cy.wrap(cc).click();
cy.get(
'.m-draggableList__list .m-proSettings__field .m-proSettings__dragDropRow--input'
)
.should('be.visible')
.within($el => {
for (let i = $el.length - 1; i >= 0; i--) {
// flexInput. Start from the last one
let c = $el[i];
for (let j = 0; j < c.children.length; j++) {
// inputs and the X button
let cc = c.children[j];
if (cc.nodeName === 'I') {
// if it's the X button, click on it
cy.wrap(cc).click();
}
}
}
}
});
});
}
function addTag(label, tag, index) {
cy.contains('+ Add Tag')
.click();
cy.contains('+ Add Tag').click();
cy.get(`#tag-label-${index}`)
.clear()
......@@ -89,8 +100,7 @@ context('Pro Page', () => {
}
function addFooterLink(label, link, index) {
cy.contains('Add Link')
.click();
cy.contains('Add Link').click();
cy.get(`#footer_link-title-${index}`)
.clear()
......@@ -116,42 +126,51 @@ context('Pro Page', () => {
});
it('should load the feed tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/activities/top**").as("activities");
cy.route('GET', '**/api/v2/pro/content/*/activities/top**').as(
'activities'
);
cy.contains('Feed')
.click()
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@activities')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the videos tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/videos/top**").as("videos");
cy.route('GET', '**/api/v2/pro/content/*/videos/top**').as('videos');
cy.contains('Videos')
.click()
.wait('@videos').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@videos')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the images tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/images/top**").as("images");
cy.route('GET', '**/api/v2/pro/content/*/images/top**').as('images');
cy.contains('Images')
.click()
.wait('@images').then((xhr) => {
expect(xhr.status).to.equal(200);
});
.wait('@images')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
// should have sub-categories
cy.get('m-pro--channel--categories > .m-proChannel__category').each(($el, $index) => {
let c = categories.slice(0);
c.unshift({ label: 'All', tag: '#all' });
expect($el.text()).to.contain(c[$index].label);
});
cy.get('m-pro--channel--categories > .m-proChannel__category').each(
($el, $index) => {
let c = categories.slice(0);
c.unshift({ label: 'All', tag: '#all' });
expect($el.text()).to.contain(c[$index].label);
}
);
cy.get('m-pro--channel .m-overlay-modal').should('not.be.visible');
// click on tile
cy.get('.m-proChannelListContent__list li:first-child m-pro--channel-tile').click();
cy.get(
'.m-proChannelListContent__list li:first-child m-pro--channel-tile'
).click();
cy.wait(200);
// media modal should appear
......@@ -159,35 +178,41 @@ context('Pro Page', () => {
// close media modal
cy.get('m-pro--channel .m-overlay-modal--close').click();
})
});
it('should load the articles tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/blogs/top**").as("blogs");
cy.route('GET', '**/api/v2/pro/content/*/blogs/top**').as('blogs');
cy.contains('Articles')
.click()
.wait('@blogs').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@blogs')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the groups tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/groups/top**").as("groups");
cy.route('GET', '**/api/v2/pro/content/*/groups/top**').as('groups');
cy.contains('Groups')
.click()
.wait('@groups').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@groups')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should have a footer', () => {
// should have a footer text
cy.get('.m-proChannelFooter__text').contains('This is the footer text');
// should have footer links
cy.get('.m-proChannel__footer .m-proChannelFooter .m-proChannelFooter__link').should('be.visible').each(($el, $index) => {
expect($el.text()).to.contain(footerLinks[$index].label);
expect($el.attr('href')).to.contain(footerLinks[$index].link);
});
})
cy.get(
'.m-proChannel__footer .m-proChannelFooter .m-proChannelFooter__link'
)
.should('be.visible')
.each(($el, $index) => {
expect($el.text()).to.contain(footerLinks[$index].label);
expect($el.attr('href')).to.contain(footerLinks[$index].link);
});
});
}
})
});
......@@ -3,30 +3,31 @@
* @desc E2E testing for Minds Pro's settings.
*/
context('Pro Settings', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
if (Cypress.env().pro_password) {
// required to run tests against pro user only.
const title = '#title';
const headline = '#headline';
const previewButton = '.m-proSettings__previewBtn';
const activityContainer = 'minds-activity';
const footerText = '#footer_text';
const theme = {
primaryColor: '#primary_color',
plainBackgroundColor: '#plain_background_color',
textColor: '#textColor',
primaryColor: '#primaryColor',
plainBackgroundColor: '#plainBgColor',
schemeLight: '#scheme_light',
schemeDark: '#scheme_dark',
aspectRatio: {
169: '#tile_ratio_16\:9', // 16:9
1610: '#tile_ratio_16\:10', // 16:10
43: '#tile_ratio_4\:3', // 4:3
11: '#tile_ratio_1\:1' , // 1:1
169: '#tile_ratio_16:9', // 16:9
1610: '#tile_ratio_16:10', // 16:10
43: '#tile_ratio_4:3', // 4:3
11: '#tile_ratio_1:1', // 1:1
},
}
};
const hashtags = {
labelInput0: '#tag-label-0',
labelInput0: '#tag-label-0',
hashtagInput0: '#tag-tag-0',
labelInput1: '#tag-label-1',
labelInput1: '#tag-label-1',
hashtagInput1: '#tag-tag-1',
label1: 'label1',
label2: 'label2',
......@@ -34,40 +35,47 @@ context('Pro Settings', () => {
hashtag1: '#hashtag1',
hashtag2: '#hashtag2',
hashtag3: '#hashtag3',
}
};
const footer = {
hrefInput: `#footer_link-href-0`,
titleInput: `#footer_link-title-0`,
}
};
const strings = {
title: "Minds Pro E2E",
headline: "This headline is a test",
footer: "This is a footer",
footerTitle: "Minds",
title: 'Minds Pro E2E',
headline: 'This headline is a test',
footer: 'This is a footer',
footerTitle: 'Minds',
footerHref: 'https://www.minds.com/',
}
};
before(() => {
cy.login(true, Cypress.env().pro_username, Cypress.env().pro_password);
});
after(() => {
cy.visit("/pro/settings")
// cy.visit(`/${Cypress.env().username}`);
cy.visit('/pro/' + Cypress.env().pro_username + '/settings/hashtags')
.location('pathname')
.should('eq', '/pro/settings');
.should(
'eq',
'/pro/' + Cypress.env().pro_username + '/settings/hashtags'
);
clearHashtags();
});
beforeEach(()=> {
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.route('POST', '**/api/v2/pro/settings').as('settings');
cy.visit("/pro/settings")
cy.visit('/pro/' + Cypress.env().pro_username + '/settings/general')
.location('pathname')
.should('eq', '/pro/settings');
.should(
'eq',
'/pro/' + Cypress.env().pro_username + '/settings/general'
);
});
it('should update the title and headline', () => {
......@@ -84,40 +92,36 @@ context('Pro Settings', () => {
saveAndPreview();
//check tab title.
cy.title()
.should('eq', strings.title+' - '+strings.headline+" | Minds");
cy.title().should(
'eq',
strings.title + ' - ' + strings.headline + ' | Minds'
);
});
// Need to find a way around the color input in Cypress.
it('should allow the user to set a dark theme for posts', () => {
cy.contains('Theme')
.click();
cy.contains('Theme').click();
cy.get(theme.schemeDark)
.click();
cy.get(theme.schemeDark).click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.contains('Feed').click();
cy.get(activityContainer)
.should('have.css', 'background-color')
.and('eq', 'rgb(35, 35, 35)');
.should('have.css', 'background-color')
.and('eq', 'rgb(35, 35, 35)');
});
it('should allow the user to set a light theme for posts', () => {
cy.contains('Theme')
.click();
cy.contains('Theme').click();
cy.get(theme.schemeLight)
.click();
cy.get(theme.schemeLight).click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.contains('Feed').click();
cy.get(activityContainer)
.should('have.css', 'background-color')
......@@ -125,12 +129,10 @@ context('Pro Settings', () => {
});
it('should allow the user to set category hashtags', () => {
cy.contains('Hashtags')
.click();
cy.contains('Hashtags').click();
cy.contains('Add').click();
cy.contains('+ Add Tag')
.click();
cy.get(hashtags.labelInput0)
.clear()
.type(hashtags.label1);
......@@ -138,10 +140,9 @@ context('Pro Settings', () => {
cy.get(hashtags.hashtagInput0)
.clear()
.type(hashtags.hashtag1);
cy.contains('+ Add Tag')
.click();
cy.contains('Add').click();
cy.get(hashtags.labelInput1)
.first()
.clear()
......@@ -151,7 +152,7 @@ context('Pro Settings', () => {
.first()
.clear()
.type(hashtags.hashtag2);
saveAndPreview();
//check the labels are present and clickable.
......@@ -160,20 +161,18 @@ context('Pro Settings', () => {
});
it('should allow the user to set footer', () => {
cy.contains('Footer')
.click();
cy.contains('Footer').click();
cy.get(footerText)
.clear()
.type(strings.footer);
cy.contains('Add Link')
.click();
cy.contains('Add Link').click();
cy.get(footer.hrefInput)
.clear()
.type(strings.footerHref);
cy.get(footer.titleInput)
.clear()
.type(strings.footerTitle);
......@@ -189,50 +188,45 @@ context('Pro Settings', () => {
function saveAndPreview() {
//save and await response
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
.click()
.wait('@settings')
.then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
//go to pro page
cy.get(previewButton)
.click();
cy.contains('View Pro Channel').click();
}
function clearHashtags() {
cy.contains('Hashtags')
.click();
cy.contains('+ Add Tag')
.click();
cy.contains('clear')
.click({multiple: true});
saveAndPreview();
cy.contains('Hashtags').click();
cy.contains('Add').click();
cy.contains('clear').click({ multiple: true });
saveAndPreview();
}
//
//
// it.only('should update the theme', () => {
// // nav to theme tab
// cy.contains('Theme')
// .click();
// cy.get(theme.plainBackgroundColor).then(elem => {
// elem.val('#00dd00');
// //save and await response
// cy.contains('Save')
// .click()
// .click()
// .wait('@settings').then((xhr) => {
// expect(xhr.status).to.equal(200);
// expect(xhr.response.body).to.deep.equal({ status: 'success' });
// });
// //go to pro page
// cy.get(previewButton)
// .click();
// });
// cy.contains('View Pro Channel').click();
// })
}
})
});
......@@ -1968,6 +1968,12 @@
"integrity": "sha512-4GbNCDs98uHCT/OMv40qQC/OpoPbYn9XdXeTiFwHBBFO6eJhYEPUu2zDKirXSbHlvDV8oZ9l8EQ+HrEx/YS9DQ==",
"dev": true
},
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
......@@ -5322,13 +5328,14 @@
"dev": true
},
"cypress": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-3.4.1.tgz",
"integrity": "sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg==",
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-3.6.1.tgz",
"integrity": "sha512-6n0oqENdz/oQ7EJ6IgESNb2M7Bo/70qX9jSJsAziJTC3kICfEMmJUlrAnP9bn+ut24MlXQST5nRXhUP5nRIx6A==",
"dev": true,
"requires": {
"@cypress/listr-verbose-renderer": "0.4.1",
"@cypress/xvfb": "1.2.4",
"@types/sizzle": "2.3.2",
"arch": "2.1.1",
"bluebird": "3.5.0",
"cachedir": "1.3.0",
......@@ -5355,6 +5362,7 @@
"request-progress": "3.0.0",
"supports-color": "5.5.0",
"tmp": "0.1.0",
"untildify": "3.0.3",
"url": "0.11.0",
"yauzl": "2.10.0"
},
......@@ -19115,6 +19123,12 @@
}
}
},
"untildify": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz",
"integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==",
"dev": true
},
"upath": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
......
......@@ -63,7 +63,7 @@
"@types/jasminewd2": "~2.0.4",
"@types/node": "~10.12.18",
"codelyzer": "^4.5.0",
"cypress": "^3.4.1",
"cypress": "^3.6.1",
"cypress-file-upload": "^3.3.3",
"gulp": "~4.0.0",
"gulp-autoprefixer": "^6.0.0",
......
......@@ -121,6 +121,9 @@ import { PageLayoutComponent } from './components/page-layout/page-layout.compon
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
import { ShadowboxHeaderComponent } from './components/shadowbox-header/shadowbox-header.component';
import { FormDescriptorComponent } from './components/form-descriptor/form-descriptor.component';
import { FormToastComponent } from './components/form-toast/form-toast.component';
import { ShadowboxSubmitButtonComponent } from './components/shadowbox-submit-button/shadowbox-submit-button.component';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -232,6 +235,9 @@ PlotlyModule.plotlyjs = PlotlyJS;
DashboardLayoutComponent,
ShadowboxLayoutComponent,
ShadowboxHeaderComponent,
FormDescriptorComponent,
FormToastComponent,
ShadowboxSubmitButtonComponent,
],
exports: [
MINDS_PIPES,
......@@ -327,6 +333,9 @@ PlotlyModule.plotlyjs = PlotlyJS;
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
FormDescriptorComponent,
FormToastComponent,
ShadowboxSubmitButtonComponent,
],
providers: [
SiteService,
......
......@@ -15,6 +15,13 @@ m-dashboardLayout {
position: relative;
display: block;
width: 100%;
a {
font-weight: 400;
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
}
@media screen and (max-width: $min-tablet) {
......
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
@Component({
selector: 'm-dashboardLayout',
templateUrl: './dashboard-layout.component.html',
})
export class DashboardLayoutComponent implements OnInit {
export class DashboardLayoutComponent {
constructor() {}
ngOnInit() {}
}
<div
class="m-draggableList__listItem m-draggableList__listHeader"
*ngIf="headers"
(click)="clickedHeaderRow($event)"
>
<ng-container *ngFor="let header of headers">
<div class="m-draggableList__cell">{{ header | titlecase }}</div>
</ng-container>
<div class="m-draggableList__cell"></div>
</div>
<ul
dndDropzone
[dndHorizontal]="false"
[dndEffectAllowed]="dndEffectAllowed"
(dndStart)="dragging = true"
(dndDrop)="onDrop($event)"
class="m-draggableList__list"
[ngClass]="{ dragging: dragging }"
>
<div class="dndPlaceholder" dndPlaceholderRef></div>
<li
*ngFor="let item of data; let i = index; trackBy: trackByFunction"
[dndDraggable]="item"
[dndEffectAllowed]="'move'"
[dndDragImageOffsetFunction]="dragImageOffsetRight"
class="m-draggableList__listItem"
>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ item: item, i: i }"
></ng-container>
<div class="m-draggableList__cell">
<i class="handle material-icons" dndHandle>open_with</i>
<i class="material-icons" (click)="removeItem(i)">
clear
</i>
</div>
</li>
</ul>
@import 'themes';
m-draggable-list {
m-draggableList {
width: 100%;
@include m-theme() {
box-shadow: 0 1px 4px 0 rgba(themed($m-black), 0.1);
}
ul.m-draggableList__list {
width: 100%;
list-style: none;
padding: 0;
padding-inline-start: 0;
margin: 0;
display: flex;
flex-direction: column;
transition: all ease 300ms;
&.dndDragover {
padding-top: 16px;
padding-bottom: 16px;
// padding-top: 16px;
// padding-bottom: 16px;
@include m-theme() {
background-color: rgba(themed($m-black), 0.05);
box-shadow: 0 1px 4px 0 rgba(themed($m-black), 0.1);
}
}
&.dragging {
li.m-draggableList__listItem {
&:first-child {
@include m-theme() {
border-top: 1px solid themed($m-grey-50);
}
}
}
}
}
li.m-draggableList__listItem {
padding: 8px;
border: 1px solid #ddd;
.m-draggableList__listItem {
display: flex;
align-items: center;
list-style-type: none;
padding: 0;
margin: 0;
@include m-theme() {
border: 1px solid themed($m-grey-50);
color: themed($m-grey-800);
}
// &:first-child {
&:not(.m-draggableList__listHeader) {
@include m-theme() {
border-top: none;
}
}
// }
&.m-draggableList__listHeader {
@include m-theme() {
// border-bottom: none;
color: themed($m-grey-300);
}
}
}
input.m-draggableList__cell {
width: 0;
min-width: 0;
}
.m-draggableList__cell {
padding: 10px 20px;
flex: 1 1 0px;
box-sizing: border-box;
@include m-theme() {
border: none;
border-right: 1px solid themed($m-grey-50);
background-color: themed($m-white);
}
&input {
width: 0;
min-width: 0;
}
&:last-child {
//icon cell
padding: 10px 15px;
flex: 0 0 80px;
max-width: 80px;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
.handle {
@include m-theme() {
color: themed($grey-600);
}
@include m-theme() {
border-right: none;
}
}
}
i {
cursor: pointer;
width: auto;
height: auto;
transition: all 0.3s ease;
@include m-theme() {
color: themed($m-grey-300);
}
&.handle {
font-size: 20px;
padding-right: 8px;
@include m-theme() {
}
}
&:hover {
transform: scale(1.15);
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.dndPlaceholder {
min-height: 100px;
@include m-theme() {
border: 1px dashed rgba(themed($m-grey-100), 0.8);
}
}
}
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
import { DndDropEvent, EffectAllowed } from 'ngx-drag-drop';
import {
Component,
ContentChild,
Input,
TemplateRef,
Output,
EventEmitter,
ChangeDetectorRef,
} from '@angular/core';
import {
DndDropEvent,
EffectAllowed,
DndDragImageOffsetFunction,
} from 'ngx-drag-drop';
@Component({
selector: 'm-draggable-list',
template: `
<ul
dndDropzone
[dndHorizontal]="false"
[dndEffectAllowed]="dndEffectAllowed"
(dndDrop)="onDrop($event)"
class="m-draggableList__list"
>
<div
class="dndPlaceholder"
dndPlaceholderRef
style="min-height:100px;border:1px dashed green;background-color:rgba(0, 0, 0, 0.1)"
></div>
<li
*ngFor="let item of data; let i = index; trackBy: trackByFunction"
[dndDraggable]="item"
[dndEffectAllowed]="'move'"
class="m-draggableList__listItem"
>
<i class="handle material-icons" dndHandle>reorder</i>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ item: item, i: i }"
></ng-container>
</li>
</ul>
`,
selector: 'm-draggableList',
templateUrl: 'list.component.html',
})
export class DraggableListComponent {
@Input() data: Array<any>;
@Input() dndEffectAllowed: EffectAllowed = 'copyMove';
@Input() id: string;
@Input() headers: string[];
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
@Output() emptyListHeaderRowClicked: EventEmitter<any> = new EventEmitter();
@Output() arrayChanged: EventEmitter<any> = new EventEmitter();
dragging: boolean = false;
trackByFunction(index, item) {
return this.id ? item[this.id] + index : index;
}
constructor(private cd: ChangeDetectorRef) {}
onDrop(event: DndDropEvent) {
this.dragging = false;
if (
this.data &&
(event.dropEffect === 'copy' || event.dropEffect === 'move')
......@@ -50,7 +43,7 @@ export class DraggableListComponent {
let dragIndex = this.data.findIndex(
item => event.data[this.id] === item[this.id]
);
let dropIndex = event.index || this.data.length;
let dropIndex = event.index;
// remove element
this.data.splice(dragIndex, 1);
......@@ -60,6 +53,28 @@ export class DraggableListComponent {
}
this.data.splice(dropIndex, 0, event.data);
this.arrayChanged.emit(this.data);
}
}
removeItem(index) {
this.data.splice(index, 1);
this.arrayChanged.emit(this.data);
}
clickedHeaderRow($event) {
if (this.data.length === 0) {
this.emptyListHeaderRowClicked.emit($event);
}
}
dragImageOffsetRight: DndDragImageOffsetFunction = (
event: DragEvent,
dragImage: HTMLElement
) => {
return {
x: dragImage.offsetWidth - 57,
y: event.offsetY + 10,
};
};
}
<div class="m-formDescriptor" i18n>
<ng-content></ng-content>
</div>
.m-formDescriptor {
font-size: 15px;
line-height: 20px;
@include m-theme() {
color: themed($m-blue);
border-left: 2px solid themed($m-blue);
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardLayoutComponent } from './dashboard-layout.component';
import { FormDescriptorComponent } from './form-descriptor.component';
describe('DashboardLayoutComponent', () => {
let component: DashboardLayoutComponent;
let fixture: ComponentFixture<DashboardLayoutComponent>;
describe('FormDescriptorComponent', () => {
let component: FormDescriptorComponent;
let fixture: ComponentFixture<FormDescriptorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DashboardLayoutComponent],
declarations: [FormDescriptorComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardLayoutComponent);
fixture = TestBed.createComponent(FormDescriptorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
......
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-formDescriptor',
templateUrl: './form-descriptor.component.html',
})
export class FormDescriptorComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
<div class="m-formToast__toastsContainer">
<ng-container *ngFor="let toast of toasts; let i = index">
<div
class="m-formToast__wrapper"
[ngClass]="{ dismissed: toast.dismissed }"
>
<i
class="material-icons m-formToast__icon--success"
*ngIf="toast.type === 'success'"
>check</i
>
<i
class="material-icons m-formToast__icon--error"
*ngIf="toast.type === 'error'"
>warning</i
>
<i
class="material-icons m-formToast__icon--warning"
*ngIf="toast.type === 'warning'"
>warning</i
>
<i
class="material-icons m-formToast__icon--info"
*ngIf="toast.type === 'info'"
></i>
<p i18n>{{ toast.message }}</p>
<div class="m-formToast__iconWrapper">
<i
class="material-icons m-formToast__icon--close"
(click)="toast.dismissed = true"
>clear</i
>
</div>
</div>
</ng-container>
</div>
m-formToast {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
max-width: 522px;
width: 60%;
transition: all 0.3s ease;
}
.m-formToast__toastsContainer {
display: flex;
flex-flow: column nowrap;
justify-content: flex-end;
}
.m-formToast__wrapper {
box-sizing: border-box;
width: 100%;
font-size: 15px;
line-height: 20px;
padding: 13px;
margin-bottom: 16px;
display: flex;
opacity: 1;
animation-name: fadeIn;
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
animation-duration: 0.4s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
@include m-theme() {
color: themed($m-grey-600);
background-color: themed($m-white);
box-shadow: 0 0 15px 0 rgba(themed($m-black), 0.2);
}
&.dismissed {
display: none;
}
p {
flex-grow: 1;
margin: 0;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
[class*='m-formToast__icon--'] {
margin-right: 10px;
}
.m-formToast__icon--success {
@include m-theme() {
color: themed($m-green-dark);
}
}
.m-formToast__icon--error {
@include m-theme() {
color: themed($m-red);
}
}
.m-formToast__icon--warning {
@include m-theme() {
color: themed($m-amber-dark);
}
}
.m-formToast__icon--info {
margin-right: 14px;
}
.m-formToast__icon--close {
cursor: pointer;
transition: all 0.2s ease-out;
@include m-theme() {
color: themed($m-grey-300);
}
&:hover {
transform: scale(1.2);
@include m-theme() {
color: themed($m-grey-100);
}
}
&:active {
@include m-theme() {
color: themed($m-grey-400);
}
}
}
@media screen and (max-width: $max-mobile) {
m-formToast {
bottom: 48px;
width: 75%;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormToastComponent } from './form-toast.component';
import { FormToastService } from '../../services/form-toast.service';
describe('FormToastComponent', () => {
let component: FormToastComponent;
let fixture: ComponentFixture<FormToastComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [FormToastComponent],
providers: [FormToastService],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FormToastComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { FormToast, FormToastService } from '../../services/form-toast.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'm-formToast',
templateUrl: './form-toast.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormToastComponent implements OnInit, OnDestroy {
toasts: FormToast[] = [];
timeoutIds: number[] = [];
subscription: Subscription;
constructor(
private service: FormToastService,
protected cd: ChangeDetectorRef
) {}
ngOnInit() {
this.subscription = this.service.onToast().subscribe(toast => {
// clear toasts when an empty toast is received
if (!toast.message) {
this.toasts = [];
return;
}
const toastIndex = this.toasts.push(toast) - 1;
console.log(toastIndex);
this.detectChanges();
const toastTimeout = setTimeout(() => {
this.toasts[toastIndex]['dismissed'] = true;
this.detectChanges();
}, 3400);
this.timeoutIds.push(setTimeout(() => toastTimeout));
});
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
ngOnDestroy() {
this.timeoutIds.forEach(id => clearTimeout(id));
this.subscription.unsubscribe();
}
}
<m-sidebarMenu [catId]="navId"></m-sidebarMenu>
<m-sidebarMenu [menu]="menu"></m-sidebarMenu>
<section class="m-pageLayout__main">
<ng-content select="[m-pageLayout__main]"></ng-content>
</section>
......@@ -2,12 +2,17 @@ m-pageLayout {
display: block;
position: relative;
width: 100%;
padding-top: 56px;
margin-bottom: 48px;
max-width: 1200px;
margin: auto;
min-height: 100%;
padding: 56px 0 48px 0;
@include m-theme() {
background-color: themed($m-white);
color: themed($m-grey-800);
}
&.isForm {
min-height: 0px;
}
.m-tooltip {
margin-left: 4px;
......@@ -18,7 +23,7 @@ m-pageLayout {
}
}
.m-tooltip--bubble {
z-index: 9999;
z-index: 99;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
......@@ -30,14 +35,14 @@ m-pageLayout {
m-sidebarMenu {
display: block;
box-sizing: border-box;
padding-left: 105px;
width: 245px;
padding-left: 20px;
@include m-theme() {
background-color: themed($m-white);
}
}
.m-pageLayout__main {
margin-left: 350px;
margin-left: 25%;
margin-right: 24px;
@include m-theme() {
......@@ -47,6 +52,12 @@ m-sidebarMenu {
}
@media screen and (max-width: $min-tablet) {
m-pageLayout {
&.isForm {
padding-bottom: 0px;
}
}
.m-pageLayout__main {
display: block;
margin: 0;
......
......@@ -8,6 +8,7 @@ import { PageLayoutComponent } from './page-layout.component';
})
class SidebarMenuComponentMock {
@Input() catId;
@Input() menu;
}
describe('PageLayoutComponent', () => {
......
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, Input, HostBinding } from '@angular/core';
import { Menu } from '../sidebar-menu/sidebar-menu.component';
@Component({
selector: 'm-pageLayout',
templateUrl: './page-layout.component.html',
})
export class PageLayoutComponent implements OnInit {
@Input() navId: string;
@Input() menu: Menu;
@HostBinding('class.isForm') @Input() isForm: boolean = false;
constructor() {}
ngOnInit() {}
......
m-shadowboxHeader {
min-height: 116px;
display: block;
a {
font-weight: 400;
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
}
.m-shadowboxHeader__section {
position: relative;
......@@ -16,8 +23,6 @@ m-shadowboxHeader {
.m-shadowboxHeader__container {
overflow-x: hidden;
overflow-y: hidden;
// display: flex;
// flex-wrap: nowrap;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
&.disable-scrollbars {
......@@ -95,8 +100,31 @@ m-shadowboxHeader {
}
}
}
.m-shadowboxLayout__header.hasTitle {
padding: 34px 28px 34px 70px;
}
.m-shadowboxHeader__title {
margin: 0 0 2px 0;
line-height: 32px;
font-weight: 500;
@include m-theme() {
color: themed($m-grey-800);
font-size: 24px;
}
}
.m-shadowboxHeader__subtitle {
line-height: 22px;
margin: 0;
@include m-theme() {
color: themed($m-grey-300);
font-size: 15px;
}
}
@media screen and (max-width: $min-tablet) {
.m-shadowboxLayout__header.hasTitle {
padding: 0 24px;
}
.m-shadowboxHeader__section {
[class*='m-shadowboxHeader__overflowScrollButton--'] {
display: none;
......
......@@ -16,10 +16,10 @@ import {
})
export class ShadowboxHeaderComponent implements AfterViewInit {
@Input() isScrollable: boolean = true;
@Input() metricActivated;
@Input() itemActivated;
@ViewChild('shadowboxHeaderContainer', { static: false })
shadowboxHeaderContainerEl: ElementRef;
shadowboxHeaderContainer;
containerEl: ElementRef;
container;
childClientWidth: number;
faderWidth = 24;
......@@ -44,7 +44,8 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
// ----------------------------------------------------
@HostListener('click', ['$event.target'])
onClick(target) {
console.log('***Clicked on: ', target);
// console.log('***Clicked on: ', target);
// find index of target
// this.slideToActiveMetric(metricIndex);
}
......@@ -73,38 +74,32 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
// TODO: figure out how to avoid test failure "Cannot read property 'clientWidth' of null"
this.childClientWidth = firstMetric ? firstMetric.clientWidth : 160;
this.shadowboxHeaderContainer = this.shadowboxHeaderContainerEl.nativeElement;
this.container = this.containerEl.nativeElement;
this.isOverflown =
this.shadowboxHeaderContainer.scrollWidth -
this.shadowboxHeaderContainer.clientWidth >
0;
this.container.scrollWidth - this.container.clientWidth > 0;
this.isAtScrollStart =
this.shadowboxHeaderContainer.scrollLeft < this.faderWidth;
this.isAtScrollStart = this.container.scrollLeft < this.faderWidth;
this.showButton.left = this.isOverflown && !this.isAtScrollStart;
this.isAtScrollEnd =
!this.isOverflown ||
this.shadowboxHeaderContainer.scrollWidth -
(this.shadowboxHeaderContainer.scrollLeft +
this.shadowboxHeaderContainer.clientWidth) <
this.container.scrollWidth -
(this.container.scrollLeft + this.container.clientWidth) <
this.faderWidth;
this.showButton.right =
this.isOverflown &&
this.shadowboxHeaderContainer.scrollLeft >= 0 &&
!this.isAtScrollEnd;
this.isOverflown && this.container.scrollLeft >= 0 && !this.isAtScrollEnd;
this.detectChanges();
}
slide(direction) {
let currentScrollLeft = this.shadowboxHeaderContainer.scrollLeft;
let currentScrollLeft = this.container.scrollLeft;
let targetScrollLeft;
let scrollEndOffset = 0;
const partiallyVisibleMetricWidth =
this.shadowboxHeaderContainer.clientWidth % this.childClientWidth;
this.container.clientWidth % this.childClientWidth;
const completelyVisibleMetricsWidth =
this.shadowboxHeaderContainer.clientWidth - partiallyVisibleMetricWidth;
this.container.clientWidth - partiallyVisibleMetricWidth;
if (direction === 'right') {
if (currentScrollLeft < this.faderWidth) {
......@@ -112,8 +107,7 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
}
targetScrollLeft = Math.min(
currentScrollLeft + completelyVisibleMetricsWidth,
this.shadowboxHeaderContainer.scrollWidth -
completelyVisibleMetricsWidth
this.container.scrollWidth - completelyVisibleMetricsWidth
);
} else {
if (this.isAtScrollEnd) {
......@@ -125,7 +119,7 @@ export class ShadowboxHeaderComponent implements AfterViewInit {
);
}
this.shadowboxHeaderContainer.scrollTo({
this.container.scrollTo({
top: 0,
left: targetScrollLeft,
behavior: 'smooth',
......
......@@ -2,12 +2,23 @@
*ngIf="hasHeader"
[isScrollable]="scrollableHeader"
[ngClass]="{ isScrollable: scrollableHeader }"
><ng-content
>
<ng-content
select=".m-shadowboxLayout__header"
ngProjectAs=".m-shadowboxLayout__header"
></ng-content
></m-shadowboxHeader>
<div class="m-shadowboxLayout__bottom">
></ng-content>
></m-shadowboxHeader
>
<div *ngIf="!isForm" class="m-shadowboxLayout__bottom">
<ng-content select=".m-shadowboxLayout__body"></ng-content>
<ng-content select=".m-shadowboxLayout__footer"></ng-content>
</div>
<ng-container *ngIf="isForm">
<ng-content
class="m-shadowboxLayout__bottom"
select=".m-shadowboxLayout__bottom"
>
</ng-content>
</ng-container>
......@@ -12,27 +12,121 @@ m-shadowboxHeader.isScrollable {
}
.m-shadowboxLayout__bottom {
position: relative;
transition: all 0.3s ease;
@include m-theme() {
border-top: 1px solid rgba(themed($m-grey-50), 0.5);
background-color: themed($m-white);
}
}
.m-shadowboxLayout__footer {
min-height: 104px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
width: 100%;
@include m-theme() {
background-color: rgba(themed($m-grey-50), 0.25);
border-top: 1px solid rgba(themed($m-grey-50), 0.6);
}
> * {
margin: 30px 68px 30px 0;
}
.m-shadowboxLayout__button {
cursor: pointer;
padding: 10px 20px;
background-color: #4fc3a9;
min-height: 43px;
border: 0;
transition: all 0.2s ease;
border-radius: 2px;
outline: 0;
@include m-theme() {
color: themed($m-white);
}
&:hover {
transform: scale(1.02);
background-color: #55ccb2;
@include m-theme() {
box-shadow: 0 3px 3px -2px rgba(themed($m-black), 0.2),
0 2px 5px 0 rgba(themed($m-black), 0.14),
0 1px 7px 0 rgba(themed($m-black), 0.12);
}
}
&:active {
transform: scale(0.999);
@include m-theme() {
box-shadow: 0 3px 2px -2px rgba(themed($m-black), 0.2),
0 2px 3px 0 rgba(themed($m-black), 0.14),
0 1px 5px 0 rgba(themed($m-black), 0.12);
}
}
&:disabled,
&[disabled] {
cursor: default;
@include m-theme() {
background-color: themed($m-grey-100);
}
}
p {
margin: 0;
font-size: 17px;
font-weight: 300;
span {
font-size: 72px;
}
}
}
button {
outline: 0;
}
@keyframes blink {
0% {
opacity: 0.2;
transform: scale(2.1);
font-size: 77px;
}
20% {
opacity: 1;
transform: scale(1.2);
font-size: 90px;
}
100% {
opacity: 0.2;
transform: scale(1.3);
}
}
p.m-shadowboxLayout__buttonStatus--saving {
// line-height:70px
span {
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
// ---------------------------------------
m-shadowboxLayout.isForm {
margin-top: 69px;
.m-shadowboxLayout__body {
padding: 50px 70px;
}
.m-shadowboxLayout__footer {
padding: 30px 70px;
align-items: center;
}
.m-shadowboxHeader__wrapper {
height: auto;
}
}
// ---------------------------------------
@media screen and (max-width: $min-tablet) {
m-shadowboxLayout {
@include m-theme() {
......@@ -44,4 +138,42 @@ m-shadowboxHeader.isScrollable {
border-top: 1px solid themed($m-grey-100);
}
}
m-shadowboxLayout.isForm {
margin-top: 0px;
m-shadowboxHeader {
min-height: 80px;
}
.m-shadowboxLayout__bottom {
@include m-theme() {
border-top: none;
}
}
.m-shadowboxLayout__body {
padding: 24px 24px 36px 24px;
}
.m-shadowboxLayout__footer {
justify-content: center;
padding: 24px 24px 48px 24px;
}
}
}
@media screen and (max-width: $max-mobile) {
m-shadowboxLayout.isForm {
.m-shadowboxLayout__bottom {
@include m-theme() {
border-top-color: rgba(0, 0, 0, 0);
}
}
.m-shadowboxLayout__footer {
.m-shadowboxLayout__button {
min-width: 50%;
}
}
}
m-shadowboxLayout {
@include m-theme() {
box-shadow: none;
}
}
}
import { Component, OnInit, Input } from '@angular/core';
import { Component, Input, HostBinding } from '@angular/core';
@Component({
selector: 'm-shadowboxLayout',
templateUrl: './shadowbox-layout.component.html',
})
export class ShadowboxLayoutComponent implements OnInit {
export class ShadowboxLayoutComponent {
@Input() scrollableHeader: boolean = true;
@Input() hasHeader: boolean = true;
constructor() {}
@HostBinding('class.isForm') @Input() isForm: boolean = false;
ngOnInit() {}
constructor() {}
}
<button
class="m-shadowboxSubmitButton"
type="submit"
[disabled]="disabled"
[ngClass]="{ saving: saveStatus === 'saving' }"
>
<ng-content></ng-content>
</button>
.m-shadowboxSubmitButton {
min-width: 220px;
position: relative;
cursor: pointer;
padding: 10px 20px;
background-color: #4fc3a9;
min-height: 43px;
border: 0;
transition: all 0.2s ease;
border-radius: 2px;
outline: 0;
@include m-theme() {
color: themed($m-white);
}
&:hover {
&:not(:disabled) {
transform: scale(1.02);
background-color: #55ccb2;
@include m-theme() {
box-shadow: 0 3px 3px -2px rgba(themed($m-black), 0.2),
0 2px 5px 0 rgba(themed($m-black), 0.14),
0 1px 7px 0 rgba(themed($m-black), 0.12);
}
}
}
&:active {
&:not(:disabled) {
transform: scale(0.999);
background-color: #4fc3a9;
@include m-theme() {
box-shadow: 0 3px 2px -2px rgba(themed($m-black), 0.2),
0 2px 3px 0 rgba(themed($m-black), 0.14),
0 1px 5px 0 rgba(themed($m-black), 0.12);
}
}
}
&:disabled,
&[disabled] {
cursor: default;
&:not(.saving) {
@include m-theme() {
background-color: themed($m-grey-100);
}
}
}
}
button {
outline: 0;
}
[class*='m-shadowboxSubmitButton__status'] {
font-size: 17px;
font-weight: 300;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
transform: scale(1.05);
}
100% {
opacity: 0.2;
transform: scale(0.9);
}
}
.m-shadowboxSubmitButton__status--saving {
span {
display: inline-block;
height: 8px;
width: 8px;
margin: 0 6px;
border-radius: 50%;
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
@include m-theme() {
background-color: themed($m-white);
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
@media screen and (max-width: $max-mobile) {
m-shadowboxSubmitButton {
min-width: 50%;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ShadowboxSubmitButtonComponent } from './shadowbox-submit-button.component';
describe('ShadowboxSubmitButtonComponent', () => {
let component: ShadowboxSubmitButtonComponent;
let fixture: ComponentFixture<ShadowboxSubmitButtonComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ShadowboxSubmitButtonComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShadowboxSubmitButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'm-shadowboxSubmitButton',
templateUrl: './shadowbox-submit-button.component.html',
})
export class ShadowboxSubmitButtonComponent implements OnInit {
@Input() saveStatus: string = 'unsaved';
@Input() disabled: boolean = false;
constructor() {}
ngOnInit() {}
}
const sidebarMenuCategories = [
{
category: {
id: 'analytics',
label: 'Analytics',
path: '/analytics/dashboard/',
permissions: ['admin', 'user'],
},
subcategories: [
// {
// id: 'summary',
// label: 'Summary',
// permissions: ['admin', 'user'],
// },
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
},
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
],
},
// {
// category: {
// id: 'test1',
// label: 'Test1',
// permissions: ['admin', 'user'],
// path: '/somepath/bork',
// },
// subcategories: [
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
// {
// id: 'nodes2',
// label: 'Nodes2',
// permissions: ['admin'],
// },
// ],
// },
// {
// category: {
// id: 'test2',
// label: 'Test2 no subcats',
// path: '/anotherpath/test2',
// },
// },
];
export default sidebarMenuCategories;
const sidebarMenuCategories = [
{
header: {
id: 'analytics',
label: 'Analytics',
path: '/analytics/dashboard/',
permissions: ['admin', 'user'],
},
links: [
{
id: 'summary',
label: 'Summary',
permissions: ['admin'],
},
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
// path: '/some/path/outside/header/path',
},
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
],
},
];
export default sidebarMenuCategories;
<div class="m-sidebarMenu__topbar">
<i class="material-icons" (click)="mobileMenuExpanded = true">menu</i>
<div class="m-sidebarMenu__topbarHeader">
{{ cat.header.label }}
{{ menu.header.label }}
</div>
</div>
<div
......@@ -29,38 +29,38 @@
</div> -->
</div>
</a>
<!-- <ng-container *ngFor="let cat of cats"> -->
<div
class="m-sidebarMenu__catContainer expanded"
*ngIf="cat.header.permissionGranted"
class="m-sidebarMenu__menuContainer expanded"
*ngIf="menu.header.permissionGranted"
>
<!-- [ngClass]="{ expanded: cat.header.expanded }" -->
<div class="m-sidebarMenu__header">
<h3>{{ cat.header.label }}</h3>
<!-- <i
class="material-icons"
*ngIf="cat.header.expanded && cat.links"
(click)="cat.header.expanded = false"
>keyboard_arrow_up</i
> -->
<!-- <i class="material-icons" *ngIf="!cat.header.expanded && cat.links"
>keyboard_arrow_down</i
> -->
<!-- (click)="cat.header.expanded = true" -->
<h3>{{ menu.header.label }}</h3>
</div>
<nav class="m-sidebarMenu__linksContainer" *ngIf="cat.links">
<div class="m-sidebarMenu__link" *ngFor="let link of cat.links">
<a
*ngIf="link.permissionGranted"
(click)="mobileMenuExpanded = false"
[routerLink]="link.path ? '/' + link.path : '../' + link.id"
routerLinkActive="selected"
>{{ link.label }}</a
<nav class="m-sidebarMenu__linksContainer" *ngIf="menu.links">
<div class="m-sidebarMenu__link" *ngFor="let link of menu.links">
<ng-container *ngIf="link.permissionGranted">
<ng-container *ngIf="!link.newWindow">
<a
(click)="mobileMenuExpanded = false"
[routerLink]="link.path ? '/' + link.path : '../' + link.id"
routerLinkActive="selected"
[routerLinkActiveOptions]="{ exact: true }"
>
<span>{{ link.label }}</span></a
>
</ng-container>
<ng-container *ngIf="link.newWindow"
><a
[routerLink]="link.path ? ['/' + link.path] : ['../' + link.id]"
target="_blank"
class="newWindow"
><i class="material-icons">launch</i>
<span>{{ link.label }}</span></a
></ng-container
></ng-container
>
</div>
</nav>
</div>
<!-- </ng-container> -->
</div>
......@@ -7,7 +7,7 @@ m-sidebarMenu {
.m-sidebarMenu__userWrapper {
display: none;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__menuContainer {
.m-sidebarMenu__linksContainer {
display: block;
cursor: pointer;
......@@ -17,6 +17,15 @@ m-sidebarMenu {
.m-sidebarMenu__sidebar {
position: fixed;
top: 157px;
bottom: 0;
overflow-x: hidden;
overflow-y: scroll;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.m-sidebarMenu__header {
......@@ -35,7 +44,6 @@ m-sidebarMenu {
.m-sidebarMenu__linksContainer {
cursor: pointer;
display: none;
.m-sidebarMenu__link {
a {
display: block;
......@@ -45,6 +53,16 @@ m-sidebarMenu {
@include m-theme() {
color: themed($m-grey-300);
}
&.newWindow {
margin-top: 50px;
display: flex;
align-items: center;
i {
margin-right: 10px;
font-size: 13px;
font-weight: bold;
}
}
}
a.selected,
&:hover a {
......@@ -52,6 +70,9 @@ m-sidebarMenu {
color: themed($m-blue);
}
}
&:last-child {
padding-bottom: 36px;
}
}
}
}
......@@ -127,7 +148,7 @@ m-sidebarMenu {
&.mobileMenuExpanded {
left: 0;
}
.m-sidebarMenu__catContainer {
.m-sidebarMenu__menuContainer {
.m-sidebarMenu__linksContainer {
display: none;
}
......
......@@ -4,7 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { SidebarMenuComponent } from './sidebar-menu.component';
import sidebarMenuCategories from './sidebar-menu-categories.default';
import sidebarMenu from '../../../modules/pro/settings/sidebar-menu.default';
describe('SidebarMenuComponent', () => {
let component: SidebarMenuComponent;
......@@ -21,7 +21,7 @@ describe('SidebarMenuComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(SidebarMenuComponent);
component = fixture.componentInstance;
component.catId = 'analytics';
component.menu = sidebarMenu;
// component.user = sessionMock.user;
fixture.detectChanges();
});
......
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Session } from '../../../services/session';
import sidebarMenuCategories from './sidebar-menu-categories.default';
interface MenuCategory {
interface Menu {
header: MenuLink;
links?: MenuLink[];
expanded?: boolean;
}
export { MenuCategory };
export { Menu };
interface MenuLink {
id: string;
......@@ -16,6 +15,7 @@ interface MenuLink {
permissions?: string[];
permissionGranted?: boolean;
path?: string;
newWindow?: boolean;
}
export { MenuLink };
......@@ -24,11 +24,9 @@ export { MenuLink };
templateUrl: './sidebar-menu.component.html',
})
export class SidebarMenuComponent implements OnInit {
@Input() catId: string;
@Input() menu: Menu;
cat: MenuCategory;
mobileMenuExpanded = false;
// activeCat;
minds: Minds;
user;
userRoles: string[] = ['user'];
......@@ -38,38 +36,38 @@ export class SidebarMenuComponent implements OnInit {
ngOnInit() {
this.minds = window.Minds;
this.user = this.session.getLoggedInUser();
this.cat = sidebarMenuCategories.find(cat => cat.header.id === this.catId);
this.getUserRoles();
this.grantPermissionsAndFindActiveCat();
this.grantPermissions();
}
getUserRoles() {
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
// TODO: define & handle other userRole options, e.g. pro, loggedIn
if (this.minds.user.pro) {
this.userRoles.push('pro');
}
}
grantPermissionsAndFindActiveCat() {
// this.cat.forEach(this.cat => {
this.cat.header['permissionGranted'] = this.cat.header.permissions
? this.checkForRoleMatch(this.cat.header.permissions)
grantPermissions() {
this.menu.header['permissionGranted'] = this.menu.header.permissions
? this.checkForRoleMatch(this.menu.header.permissions)
: true;
if (this.cat.links) {
this.cat.links.forEach(link => {
if (this.menu.links) {
this.menu.links.forEach(link => {
link['permissionGranted'] = link.permissions
? this.checkForRoleMatch(link.permissions)
: true;
if (link.id === ':username') {
link.id = this.user.username;
}
if (link.path) {
link.path = link.path.replace(':username', this.user.username);
}
});
}
// if (location.pathname.indexOf(this.cats.header.path) !== -1) {
// this.cats.header['expanded'] = true;
// this.activeCat = this.cat;
// } else {
// this.cat.header['expanded'] = false;
// }
// });
}
checkForRoleMatch(permissionsArray) {
......
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
export interface FormToast {
type?: 'success' | 'error' | 'warning' | 'info' | null;
message?: string;
}
@Injectable()
export class FormToastService {
private subject = new Subject<FormToast>();
constructor() {}
onToast(): Observable<FormToast> {
return this.subject.asObservable();
}
success(message: string) {
const toast: FormToast = {
message: message,
type: 'success',
};
this.trigger(toast);
}
error(message: string) {
const toast: FormToast = {
message: message,
type: 'error',
};
this.trigger(toast);
}
warn(message: string) {
const toast: FormToast = {
message: message,
type: 'warning',
};
this.trigger(toast);
}
inform(message: string) {
const toast: FormToast = {
message: message,
type: 'info',
};
this.trigger(toast);
}
trigger(toast: FormToast) {
if (!toast.type) {
toast.type = 'info';
}
this.subject.next(toast);
}
}
......@@ -83,7 +83,7 @@ const routes: Routes = [
],
},
{
path: 'dashboard/',
path: 'dashboard',
redirectTo: 'dashboard/traffic',
pathMatch: 'full',
},
......
<m-pageLayout navId="analytics">
<m-pageLayout [menu]="menu">
<div class="m-analyticsDashboard" *ngIf="ready$ | async" m-pageLayout__main>
<m-dashboardLayout>
<ng-container m-dashboardLayout__header>
......@@ -34,7 +34,7 @@
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to PRO</a
>Upgrade to Pro</a
>
<a
*ngIf="
......
......@@ -53,13 +53,6 @@ m-analytics__dashboard {
@include m-theme() {
color: themed($m-grey-300);
}
a {
font-weight: 400;
text-decoration: none;
@include m-theme() {
color: themed($m-blue);
}
}
}
@media screen and (max-width: $min-tablet) {
......
......@@ -16,6 +16,8 @@ import { Session } from '../../../services/session';
import { AnalyticsDashboardService } from './dashboard.service';
import { Filter } from './../../../interfaces/dashboard';
import sidebarMenu from './sidebar-menu.default';
import { Menu } from '../../../common/components/sidebar-menu/sidebar-menu.component';
@Component({
selector: 'm-analytics__dashboard',
......@@ -24,6 +26,7 @@ import { Filter } from './../../../interfaces/dashboard';
providers: [AnalyticsDashboardService],
})
export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
menu: Menu = sidebarMenu;
paramsSubscription: Subscription;
ready$ = this.analyticsService.ready$;
......@@ -63,8 +66,6 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
this.updateCategory(cat);
if (cat === 'summary') {
this.layout = 'summary';
} else {
this.layout = 'chart';
}
});
......
const sidebarMenu = {
header: {
id: 'analytics',
label: 'Analytics',
permissions: ['admin', 'user'],
},
links: [
{
id: 'summary',
label: 'Summary',
permissions: ['admin'],
// path: '/some/path/outside/analytics/dashboard',
},
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
},
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
],
};
export default sidebarMenu;
......@@ -196,14 +196,7 @@ export class ChannelSidebar {
}
get proSettingsRouterLink() {
const isAdmin = window.Minds.Admin;
const route: any[] = ['/pro/settings'];
if (isAdmin) {
route.push({ user: this.user.username });
}
const route: any[] = ['/pro/' + this.user.username + '/settings'];
return route;
}
}
......@@ -23,7 +23,7 @@
<m-pro__subscribeButton (onAction)="closeMenu()"></m-pro__subscribeButton>
</li>
<li>
<li class="m-proHamburgerMenu__linkWrapper">
<a
[routerLink]="feedRouterLink"
routerLinkActive="m-proHamburgerMenu__item--active"
......@@ -91,7 +91,7 @@
<li class="m-proHamburgerMenu__spacer"></li>
<ng-container *ngIf="items && items.length">
<li *ngFor="let item of items">
<li *ngFor="let item of items" class="m-proHamburgerMenu__linkWrapper">
<a
[class.m-proHamburgerMenu__item--active]="item.isActive()"
(click)="item.onClick(); closeMenu()"
......
......@@ -26,6 +26,7 @@ m-pro__hamburger-menu {
top: 30px;
right: 40px;
padding: 8px;
cursor: pointer;
}
> ul {
......@@ -47,7 +48,9 @@ m-pro__hamburger-menu {
text-transform: uppercase;
font-weight: 700;
letter-spacing: 1px;
&.m-proHamburgerMenu__linkWrapper {
cursor: pointer;
}
&.m-proHamburgerMenu__logo {
height: 100px;
......@@ -114,3 +117,7 @@ m-pro__hamburger-menu {
body.hamburger-menu--open {
overflow: hidden;
}
.m-proHamburgerMenu__trigger {
cursor: pointer;
}
......@@ -45,9 +45,11 @@ const routes: Routes = [
component: ProMarketingComponent,
},
{
path: 'settings',
component: ProSettingsComponent,
path: ':username/settings',
redirectTo: ':username/settings/general',
pathMatch: 'full',
},
{ path: ':username/settings/:tab', component: ProSettingsComponent },
{
path: ':username',
component: ProChannelComponent,
......
const sidebarMenu = {
header: {
id: 'pro_settings',
label: 'Pro Settings',
permissions: ['pro'],
},
links: [
{
id: 'general',
label: 'General',
},
{
id: 'theme',
label: 'Theme',
},
{
id: 'assets',
label: 'Assets',
},
{
id: 'hashtags',
label: 'Hashtags',
},
{
id: 'footer',
label: 'Footer',
},
{
id: 'domain',
label: 'Domain',
},
{
id: 'payouts',
label: 'Payouts',
},
{
id: 'subscription',
label: 'Pro Subscription',
path: 'pro',
},
{
id: ':username',
label: 'View Pro Channel',
path: 'pro/:username',
newWindow: true,
},
],
};
export default sidebarMenu;
......@@ -109,6 +109,17 @@
<span i18n="@@M__ACTION__SUGGESTED_HASHTAGS">Suggested Hashtags</span>
</a> -->
<a
class="m-page--sidebar--navigation--item"
*ngIf="session.getLoggedInUser().pro"
[routerLink]="[
'/pro/' + session.getLoggedInUser().username + '/settings'
]"
>
<i class="material-icons">business_center</i>
<span i18n>Pro Settings</span>
</a>
<a
class="m-page--sidebar--navigation--item"
(click)="openReferralsModal()"
......
......@@ -46,6 +46,7 @@ import { AuthService } from './auth.service';
import { SiteService } from '../common/services/site.service';
import { SessionsStorageService } from './session-storage.service';
import { DiagnosticsService } from './diagnostics.service';
import { FormToastService } from '../common/services/form-toast.service';
export const MINDS_PROVIDERS: any[] = [
SiteService,
......@@ -234,4 +235,5 @@ export const MINDS_PROVIDERS: any[] = [
},
DiagnosticsService,
AuthService,
FormToastService,
];