Skip to content
Commits on Source (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",
......
......@@ -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;
......