Merge pull request #5 from sartography/feature/fake-sso
Feature/fake sso
This commit is contained in:
commit
437e76a007
|
@ -26,10 +26,9 @@
|
|||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
|
||||
"node_modules/bpmn-js/dist/assets/diagram-js.css",
|
||||
"node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css",
|
||||
"src/styles.css"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
|
@ -145,8 +144,7 @@
|
|||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
|
||||
"src/styles.css"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"assets": [
|
||||
|
|
|
@ -7,9 +7,24 @@ describe('workspace-project App', () => {
|
|||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display diagram', () => {
|
||||
it('should display fake sign-in screen', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getDiagramContainer()).toBeTruthy();
|
||||
expect(page.getText('h1')).toEqual('FAKE UVA NETBADGE SIGN IN (FOR TESTING ONLY)');
|
||||
});
|
||||
|
||||
it('should click sign-in and navigate to home screen', () => {
|
||||
page.clickAndExpectRoute('#sign_in', '/');
|
||||
expect(page.getElements('app-file-list').count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display diagram', async () => {
|
||||
const el = await page.getElement('app-file-list mat-list-item');
|
||||
const specId = await el.getAttribute('data-workflow-spec-id');
|
||||
const fileMetaId = await el.getAttribute('data-file-meta-id');
|
||||
const expectedRoute = `/modeler/${specId}/${fileMetaId}`;
|
||||
|
||||
page.clickAndExpectRoute('app-file-list mat-list-item', expectedRoute);
|
||||
expect(page.getElements('.diagram-container').count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should show dialog to open a diagram file');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
import {browser, by, element, ElementArrayFinder, ElementFinder, ExpectedConditions} from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
|
@ -8,4 +8,91 @@ export class AppPage {
|
|||
getDiagramContainer() {
|
||||
return element(by.css('app-root .diagram-container'));
|
||||
}
|
||||
|
||||
clickAndExpectRoute(clickSelector: string, expectedRoute: string | RegExp) {
|
||||
this.waitForClickable(clickSelector);
|
||||
this.clickElement(clickSelector);
|
||||
if (typeof expectedRoute === 'string') {
|
||||
expect(this.getRoute()).toEqual(expectedRoute);
|
||||
} else {
|
||||
expect(this.getRoute()).toMatch(expectedRoute);
|
||||
}
|
||||
}
|
||||
|
||||
clickElement(selector: string) {
|
||||
this.waitForClickable(selector);
|
||||
this.scrollTo(selector);
|
||||
this.focus(selector);
|
||||
return this.getElement(selector).click();
|
||||
}
|
||||
|
||||
closeTab() {
|
||||
return browser.close();
|
||||
}
|
||||
|
||||
focus(selector: string) {
|
||||
return browser.controlFlow().execute(() => {
|
||||
return browser.executeScript('arguments[0].focus()', this.getElement(selector).getWebElement());
|
||||
});
|
||||
}
|
||||
|
||||
getElement(selector: string): ElementFinder {
|
||||
return element.all(by.css(selector)).first();
|
||||
}
|
||||
|
||||
getElements(selector: string): ElementArrayFinder {
|
||||
return element.all(by.css(selector));
|
||||
}
|
||||
|
||||
getLocalStorageVar(name: string) {
|
||||
return browser.executeScript(`return window.localStorage.getItem('${name}');`);
|
||||
}
|
||||
|
||||
getNumTabs() {
|
||||
return browser.getAllWindowHandles().then(wh => {
|
||||
return wh.length;
|
||||
});
|
||||
}
|
||||
|
||||
async getRoute() {
|
||||
const url = await this.getUrl();
|
||||
return '/' + url.split(browser.baseUrl)[1];
|
||||
}
|
||||
|
||||
getText(selector: string) {
|
||||
return element(by.css(selector)).getText() as Promise<string>;
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
return browser.getCurrentUrl();
|
||||
}
|
||||
|
||||
scrollTo(selector: string) {
|
||||
browser.controlFlow().execute(() => {
|
||||
browser.executeScript('arguments[0].scrollIntoView(false)', this.getElement(selector).getWebElement());
|
||||
});
|
||||
}
|
||||
|
||||
setLocalStorageVar(name: string, value: string) {
|
||||
return browser.executeScript(`return window.localStorage.setItem('${name}','${value}');`);
|
||||
}
|
||||
|
||||
switchFocusToTab(tabIndex: number) {
|
||||
return browser.getAllWindowHandles().then(wh => {
|
||||
return wh.forEach((h, i) => {
|
||||
if (i === tabIndex) {
|
||||
return browser.switchTo().window(h);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
waitFor(t: number) {
|
||||
return browser.sleep(t);
|
||||
}
|
||||
|
||||
waitForClickable(selector: string) {
|
||||
const e = this.getElement(selector);
|
||||
return browser.wait(ExpectedConditions.elementToBeClickable(e), 5000);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11641,9 +11641,9 @@
|
|||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sartography-workflow-lib": {
|
||||
"version": "0.0.45",
|
||||
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.45.tgz",
|
||||
"integrity": "sha512-0c3XT5ID0gF8SM18DmD88jDwOccjaViEhJ6DWzh9irblPmvbeA/B7a1MS0i0fm6IC3pw7tdja9pBFeDXz0UOZw==",
|
||||
"version": "0.0.48",
|
||||
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.48.tgz",
|
||||
"integrity": "sha512-HeqzSbbt5J9S3FulBCUEYogXIRPkkTwnhViaxd6zEzpMDebZEt6FMCBXjVZ8e+x0griL66RNCvWzh4EXRmOkqA==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"file-saver": "^2.0.2",
|
||||
"hammerjs": "^2.0.8",
|
||||
"rxjs": "~6.5.4",
|
||||
"sartography-workflow-lib": "^0.0.45",
|
||||
"sartography-workflow-lib": "^0.0.48",
|
||||
"tslib": "^1.10.0",
|
||||
"uuid": "^3.4.0",
|
||||
"zone.js": "~0.9.1"
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
// GLOBAL SCSS VARIABLES
|
||||
$body-font-family: '"franklin-gothic-urw", sans-serif';
|
||||
$heading-font-family: '"franklin-gothic-urw-cond", sans-serif';
|
||||
|
||||
// COLOR PALETTE
|
||||
|
||||
// gray
|
||||
$brand-gray: #4e4e4e;
|
||||
$brand-gray-tint-1: #666666;
|
||||
$brand-gray-tint-2: #DADADA;
|
||||
$brand-gray-tint-3: #F1F1EF;
|
||||
$brand-gray-tint-4: scale-color($brand-gray, $lightness: 90%);
|
||||
$body-color: $brand-gray;
|
||||
$body-color-muted: $brand-gray-tint-1;
|
||||
$body-color-light: $brand-gray-tint-4;
|
||||
$brand-gray-muted: $brand-gray-tint-1;
|
||||
$brand-gray-light: $brand-gray-tint-4;
|
||||
|
||||
// primary (UVA "Jefferson Blue")
|
||||
$brand-primary: #232D4B;
|
||||
$brand-primary-tint-1: #394E79;
|
||||
$brand-primary-tint-2: #6C799C;
|
||||
$brand-primary-tint-3: #A9AFC7;
|
||||
$brand-primary-tint-4: scale-color($brand-primary, $lightness: 90%);
|
||||
$brand-primary-shade-1: #092255;
|
||||
$brand-primary-shade-2: #041D4F;
|
||||
$brand-primary-shade-3: #02194A;
|
||||
$brand-primary-shade-4: #021745;
|
||||
$brand-primary-shade-5: #03143E;
|
||||
$brand-primary-muted: $brand-primary-tint-1;
|
||||
$brand-primary-light: $brand-primary-tint-4;
|
||||
|
||||
// accent (UVA "Rotunda Orange")
|
||||
$brand-accent: #E57200;
|
||||
$brand-accent-tint-1: #F69350;
|
||||
$brand-accent-tint-2: #FAB584;
|
||||
$brand-accent-tint-3: #FDD8BB;
|
||||
$brand-accent-tint-4: scale-color($brand-accent, $lightness: 90%);
|
||||
$brand-accent-shade-1: #E76E25;
|
||||
$brand-accent-shade-2: #DD6923;
|
||||
$brand-accent-shade-3: #D36421;
|
||||
$brand-accent-shade-4: #C8601F;
|
||||
$brand-accent-shade-5: #C05B1D;
|
||||
$brand-accent-muted: $brand-accent-tint-1;
|
||||
$brand-accent-light: $brand-accent-tint-4;
|
||||
|
||||
// warning (UVA "Emergency Red")
|
||||
$brand-warning: #DF1E43;
|
||||
$brand-warning-muted: desaturate(scale-color($brand-warning, $lightness: 30%), 20%);
|
||||
$brand-warning-light: scale-color($brand-warning, $lightness: 90%);
|
||||
$easeDuration: 300ms;
|
||||
$animationDuration: 500ms;
|
||||
$tile-height-1x: 226px;
|
||||
$tile-height-2x: 288px;
|
||||
$uva-header-height: 40px;
|
||||
$site-header-height: 64px;
|
||||
$search-bar-height-sm: 64px;
|
||||
$search-bar-height-md: 128px;
|
||||
$search-bar-height-lg: 64px;
|
||||
$header-height-sm: ($uva-header-height + $site-header-height + $search-bar-height-sm);
|
||||
$header-height-md: ($uva-header-height + $site-header-height + $search-bar-height-md);
|
||||
$header-height-lg: ($uva-header-height + $site-header-height + $search-bar-height-lg);
|
||||
|
||||
// Green
|
||||
$brand-green: #64B343;
|
||||
$brand-green-muted: #8EC774;
|
||||
$brand-green-light: #B5D9A3;
|
|
@ -0,0 +1,92 @@
|
|||
@import 'config';
|
||||
@import '../node_modules/@angular/material/theming';
|
||||
|
||||
// Define a custom typography config that overrides the font-family
|
||||
$custom-typography: mat-typography-config(
|
||||
$font-family: $body-font-family,
|
||||
$display-4: mat-typography-level(2.500rem, 1.0, 700, $heading-font-family),
|
||||
$display-3: mat-typography-level(2.250rem, 1.0, 700, $heading-font-family),
|
||||
$display-2: mat-typography-level(2.000rem, 1.0, 700, $heading-font-family),
|
||||
$display-1: mat-typography-level(1.750rem, 1.0, 700, $heading-font-family),
|
||||
$headline: mat-typography-level(48px, 1.0, 700, $heading-font-family),
|
||||
$title: mat-typography-level(1.500rem, 1.0, 700, $body-font-family),
|
||||
$subheading-2: mat-typography-level(1.375rem, 1.0, 500, $body-font-family),
|
||||
$subheading-1: mat-typography-level(1.250rem, 1.0, 500, $body-font-family),
|
||||
$body-2: mat-typography-level(1.000rem, 1.5, 500, $body-font-family),
|
||||
$body-1: mat-typography-level(1.000rem, 1.5, 500, $body-font-family),
|
||||
$caption: mat-typography-level(1.000rem, 1.5, 500, $body-font-family),
|
||||
$button: mat-typography-level(1.000rem, 1.5, 500, $body-font-family),
|
||||
$input: mat-typography-level(1.000rem, 1.5, 500, $body-font-family)
|
||||
);
|
||||
|
||||
$mat-blue: (
|
||||
50: #f1f5f7,
|
||||
100: #b3c1d3,
|
||||
200: $brand-primary-tint-3,
|
||||
300: $brand-primary-tint-2,
|
||||
400: $brand-primary-tint-1,
|
||||
500: $brand-primary,
|
||||
600: $brand-primary-shade-1,
|
||||
700: $brand-primary-shade-2,
|
||||
800: $brand-primary-shade-3,
|
||||
900: $brand-primary-shade-4,
|
||||
A100: #b3c1d3,
|
||||
A200: $brand-primary-tint-3,
|
||||
A400: $brand-primary-tint-2,
|
||||
A700: $brand-primary-tint-1,
|
||||
contrast: (
|
||||
50: $body-color,
|
||||
100: $body-color,
|
||||
200: $body-color,
|
||||
300: #ffffff,
|
||||
400: #ffffff,
|
||||
500: #ffffff,
|
||||
600: #ffffff,
|
||||
700: #ffffff,
|
||||
800: #ffffff,
|
||||
900: #ffffff,
|
||||
A100: $body-color,
|
||||
A200: #ffffff,
|
||||
A400: #ffffff,
|
||||
A700: #ffffff,
|
||||
)
|
||||
);
|
||||
|
||||
$mat-orange: (
|
||||
50: #fceee0,
|
||||
100: $brand-accent-tint-3,
|
||||
200: $brand-accent-tint-2,
|
||||
300: $brand-accent-tint-1,
|
||||
400: $brand-accent,
|
||||
500: $brand-accent-shade-1,
|
||||
600: $brand-accent-shade-2,
|
||||
700: $brand-accent-shade-3,
|
||||
800: $brand-accent-shade-4,
|
||||
900: $brand-accent-shade-5,
|
||||
A100: #fceee0,
|
||||
A200: $brand-accent-tint-3,
|
||||
A400: $brand-accent-tint-2,
|
||||
A700: $brand-accent-tint-1,
|
||||
contrast: (
|
||||
50: $body-color,
|
||||
100: $body-color,
|
||||
200: $body-color,
|
||||
300: $body-color,
|
||||
400: $body-color,
|
||||
500: #ffffff,
|
||||
600: #ffffff,
|
||||
700: #ffffff,
|
||||
800: #ffffff,
|
||||
900: #ffffff,
|
||||
A100: $body-color,
|
||||
A200: $body-color,
|
||||
A400: $body-color,
|
||||
A700: $body-color,
|
||||
)
|
||||
);
|
||||
|
||||
$cr-connect-primary: mat-palette($mat-blue);
|
||||
$cr-connect-accent: mat-palette($mat-orange);
|
||||
$cr-connect-warn: mat-palette($mat-red);
|
||||
$cr-connect-theme: mat-light-theme($cr-connect-primary, $cr-connect-accent, $cr-connect-warn);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// tslint:disable-next-line:max-line-length
|
||||
export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
|
||||
|
||||
export default EMAIL_REGEX;
|
|
@ -0,0 +1,36 @@
|
|||
import {FormControl} from '@angular/forms';
|
||||
import {ValidateEmail} from './email.validator';
|
||||
|
||||
describe('ValidateEmail', () => {
|
||||
const control = new FormControl();
|
||||
|
||||
it('should return an error for an invalid email address', () => {
|
||||
const emailsToTest = [
|
||||
'124535',
|
||||
'not an email address',
|
||||
'@gmail.com',
|
||||
'incomplete@domain',
|
||||
'tooshort@tld.c',
|
||||
];
|
||||
|
||||
for (const email of emailsToTest) {
|
||||
control.setValue(email);
|
||||
expect(ValidateEmail(control)).toEqual({email: true});
|
||||
}
|
||||
});
|
||||
|
||||
it('should not return an error for a valid email address', () => {
|
||||
const emailsToTest = [
|
||||
'short@tld.co',
|
||||
'simple@email.edu',
|
||||
'more+complicated@www.email-server.mail',
|
||||
'this.is.a.valid.email+address@some-random.email-server.com',
|
||||
];
|
||||
|
||||
for (const email of emailsToTest) {
|
||||
control.setValue(email);
|
||||
expect(ValidateEmail(control)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
import EMAIL_REGEX from './email.regex';
|
||||
|
||||
export function ValidateEmail(control: AbstractControl): ValidationErrors {
|
||||
if (!EMAIL_REGEX.test(control.value) && control.value && control.value !== '') {
|
||||
const error: ValidationErrors = { email: true };
|
||||
return error;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import {FormControl} from '@angular/forms';
|
||||
import {FormlyFieldConfig} from '@ngx-formly/core';
|
||||
import {FieldType} from '@ngx-formly/material';
|
||||
import * as Validators from './formly.validator';
|
||||
|
||||
describe('Formly Validators', () => {
|
||||
let control: FormControl;
|
||||
let err: Error;
|
||||
let field: FormlyFieldConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
control = new FormControl();
|
||||
err = new Error('some error');
|
||||
field = {
|
||||
type: 'email',
|
||||
templateOptions: {},
|
||||
formControl: control,
|
||||
};
|
||||
});
|
||||
|
||||
it('should validate emails', () => {
|
||||
control.setValue('');
|
||||
expect(Validators.EmailValidator(control)).toBeNull();
|
||||
|
||||
control.setValue('a@b');
|
||||
expect(Validators.EmailValidator(control)).toEqual({email: true});
|
||||
|
||||
control.setValue('a@b.c');
|
||||
expect(Validators.EmailValidator(control)).toEqual({email: true});
|
||||
|
||||
control.setValue('a@b.com');
|
||||
expect(Validators.EmailValidator(control)).toBeNull();
|
||||
});
|
||||
|
||||
it('should email validator message', () => {
|
||||
control.setValue('bad_email');
|
||||
expect(Validators.EmailValidatorMessage(err, field)).toContain('bad_email');
|
||||
});
|
||||
|
||||
it('should validate URL strings', () => {
|
||||
const badUrls = [
|
||||
'bad_url',
|
||||
'http://',
|
||||
'http://bad',
|
||||
'bad.com',
|
||||
];
|
||||
badUrls.forEach(url => {
|
||||
control.setValue(url);
|
||||
expect(Validators.UrlValidator(control)).toEqual({url: true});
|
||||
});
|
||||
|
||||
const goodUrls = [
|
||||
'http://good.url.com',
|
||||
'ftp://who-uses.ftp-these-days.com',
|
||||
'https://this.is.actually:5432/a/valid/url?doncha=know#omg',
|
||||
];
|
||||
goodUrls.forEach(url => {
|
||||
control.setValue(url);
|
||||
expect(Validators.UrlValidator(control)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a url validator message', () => {
|
||||
control.setValue('bad_url');
|
||||
expect(Validators.UrlValidatorMessage(err, field)).toContain('bad_url');
|
||||
});
|
||||
|
||||
it('should validate phone numbers', () => {
|
||||
const badPhones = [
|
||||
'not a phone number',
|
||||
'54321',
|
||||
'3.1415926535897932384...',
|
||||
'(800) CALL-BRAD',
|
||||
'(540) 155-5555',
|
||||
'(999) 654-3210',
|
||||
'1234567890',
|
||||
];
|
||||
badPhones.forEach(phone => {
|
||||
control.setValue(phone);
|
||||
expect(Validators.PhoneValidator(control)).toEqual({phone: true});
|
||||
});
|
||||
|
||||
const goodPhones = [
|
||||
'12345678909',
|
||||
'1-800-555-5555',
|
||||
'(987) 654-3210',
|
||||
'(540) 555-1212 x321',
|
||||
'(540) 555-1212 ext 321',
|
||||
'(540) 555-1212 Ext. 321',
|
||||
'540.555.5555',
|
||||
'540.555.5555extension321',
|
||||
];
|
||||
goodPhones.forEach(phone => {
|
||||
control.setValue(phone);
|
||||
expect(Validators.PhoneValidator(control)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return phone validator message', () => {
|
||||
control.setValue('bad_number');
|
||||
expect(Validators.PhoneValidatorMessage(err, field)).toContain('bad_number');
|
||||
});
|
||||
|
||||
it('should return error if at least one checkbox in multicheckbox is not checked', () => {
|
||||
control.setValue({});
|
||||
expect(Validators.MulticheckboxValidator(control)).toEqual({required: true});
|
||||
|
||||
control.setValue({
|
||||
checkbox_a: true,
|
||||
checkbox_b: false,
|
||||
checkbox_c: false,
|
||||
});
|
||||
expect(Validators.MulticheckboxValidator(control)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return multicheckbox validator message', () => {
|
||||
control.setValue({});
|
||||
expect(Validators.MulticheckboxValidatorMessage(err, field))
|
||||
.toEqual('At least one of these checkboxes must be selected.');
|
||||
});
|
||||
|
||||
it('should min validation message', () => {
|
||||
field.templateOptions.min = 42;
|
||||
expect(Validators.MinValidationMessage(err, field)).toContain('42');
|
||||
});
|
||||
|
||||
it('should max validation message', () => {
|
||||
field.templateOptions.max = 42;
|
||||
expect(Validators.MaxValidationMessage(err, field)).toContain('42');
|
||||
});
|
||||
|
||||
it('should show error', () => {
|
||||
expect(Validators.ShowError(field as FieldType)).toBeFalsy();
|
||||
|
||||
control.setErrors({url: true});
|
||||
control.markAsDirty();
|
||||
expect(Validators.ShowError(field as FieldType)).toBeTruthy();
|
||||
|
||||
field.formControl = undefined;
|
||||
expect(Validators.ShowError(field as FieldType)).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import {FormControl, ValidationErrors} from '@angular/forms';
|
||||
import {FieldType, FormlyFieldConfig} from '@ngx-formly/core';
|
||||
import EMAIL_REGEX from './email.regex';
|
||||
import PHONE_REGEX from './phone.regex';
|
||||
import URL_REGEX from './url.regex';
|
||||
|
||||
export function EmailValidator(control: FormControl): ValidationErrors {
|
||||
return !control.value || EMAIL_REGEX.test(control.value) ? null : {email: true};
|
||||
}
|
||||
|
||||
export function EmailValidatorMessage(err, field: FormlyFieldConfig) {
|
||||
return `"${field.formControl.value}" is not a valid email address`;
|
||||
}
|
||||
|
||||
export function UrlValidator(control: FormControl): ValidationErrors {
|
||||
return !control.value || URL_REGEX.test(control.value) ? null : {url: true};
|
||||
}
|
||||
|
||||
export function UrlValidatorMessage(err, field: FormlyFieldConfig) {
|
||||
return `We cannot save "${field.formControl.value}". Please provide the full path, including http:// or https://`;
|
||||
}
|
||||
|
||||
export function PhoneValidator(control: FormControl): ValidationErrors {
|
||||
return !control.value || PHONE_REGEX.test(control.value) ? null : {phone: true};
|
||||
}
|
||||
|
||||
export function PhoneValidatorMessage(err, field: FormlyFieldConfig) {
|
||||
return `"${field.formControl.value}" is not a valid phone number`;
|
||||
}
|
||||
|
||||
export function MulticheckboxValidator(control: FormControl): ValidationErrors {
|
||||
if (control.value) {
|
||||
for (const key in control.value) {
|
||||
if (control.value[key] === true) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {required: true};
|
||||
}
|
||||
|
||||
export function MulticheckboxValidatorMessage(err, field: FormlyFieldConfig) {
|
||||
return 'At least one of these checkboxes must be selected.';
|
||||
}
|
||||
|
||||
export function MinValidationMessage(err, field) {
|
||||
return `This value should be more than ${field.templateOptions.min}`;
|
||||
}
|
||||
|
||||
export function MaxValidationMessage(err, field) {
|
||||
return `This value should be less than ${field.templateOptions.max}`;
|
||||
}
|
||||
|
||||
export function ShowError(field: FieldType) {
|
||||
return field.formControl &&
|
||||
field.formControl.invalid &&
|
||||
(
|
||||
field.formControl.dirty ||
|
||||
(field.options && field.options.parentForm && field.options.parentForm.submitted) ||
|
||||
(field.field && field.field.validation && field.field.validation.show)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// tslint:disable-next-line:max-line-length
|
||||
export const PHONE_REGEX = /^(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})(?:\s*(?:#|x\.?|ext\.?|extension)\s*(\d+))?$/i;
|
||||
|
||||
export default PHONE_REGEX;
|
|
@ -0,0 +1,23 @@
|
|||
export const URL_REGEX = new RegExp(
|
||||
'^' +
|
||||
'(?:(?:https?|ftp)://)' +
|
||||
'(?:\\S+(?::\\S*)?@)?' +
|
||||
'(?:' +
|
||||
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
|
||||
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
|
||||
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
||||
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
|
||||
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
|
||||
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
|
||||
'|' +
|
||||
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
|
||||
'\\.?' +
|
||||
')' +
|
||||
'(?::\\d{2,5})?' +
|
||||
'(?:[/?#]\\S*)?' +
|
||||
'$', 'i'
|
||||
);
|
||||
|
||||
export default URL_REGEX;
|
|
@ -0,0 +1,31 @@
|
|||
import { AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
|
||||
export function ValidateUrl(control: AbstractControl): ValidationErrors {
|
||||
|
||||
const urlRegEx = new RegExp(
|
||||
'^' +
|
||||
'(?:(?:https?|ftp)://)' +
|
||||
'(?:\\S+(?::\\S*)?@)?' +
|
||||
'(?:' +
|
||||
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
|
||||
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
|
||||
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
||||
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
|
||||
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
|
||||
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
|
||||
'|' +
|
||||
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
|
||||
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
|
||||
'\\.?' +
|
||||
')' +
|
||||
'(?::\\d{2,5})?' +
|
||||
'(?:[/?#]\\S*)?' +
|
||||
'$', 'i'
|
||||
);
|
||||
|
||||
if (!urlRegEx.test(control.value) && control.value && control.value !== '') {
|
||||
const error: ValidationErrors = { url: true };
|
||||
return error;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,49 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {SessionRedirectComponent} from 'sartography-workflow-lib';
|
||||
import {HomeComponent} from './home/home.component';
|
||||
import {ModelerComponent} from './modeler/modeler.component';
|
||||
import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component';
|
||||
import {SignInComponent} from './sign-in/sign-in.component';
|
||||
import {SignOutComponent} from './sign-out/sign-out.component';
|
||||
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{ path: 'modeler/:workflowSpecId', component: ModelerComponent },
|
||||
{ path: 'modeler/:workflowSpecId/:fileMetaId', component: ModelerComponent },
|
||||
{ path: '', component: WorkflowSpecListComponent },
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomeComponent
|
||||
},
|
||||
{
|
||||
path: 'modeler/:workflowSpecId',
|
||||
component: ModelerComponent
|
||||
},
|
||||
{
|
||||
path: 'modeler/:workflowSpecId/:fileMetaId',
|
||||
component: ModelerComponent
|
||||
},
|
||||
{
|
||||
path: 'sign-in',
|
||||
component: SignInComponent
|
||||
},
|
||||
{
|
||||
path: 'sign-out',
|
||||
component: SignOutComponent
|
||||
},
|
||||
{
|
||||
path: 'session/:token',
|
||||
component: SessionRedirectComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
RouterModule.forRoot(appRoutes)
|
||||
RouterModule.forRoot(routes, {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
scrollOffset: [0, 84],
|
||||
})
|
||||
],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
export class AppRoutingModule {
|
||||
}
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
<div class="mat-typography">
|
||||
<app-navbar *ngIf="isSignedIn()" class="mat-elevation-z6" id="globalHeader"></app-navbar>
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#globalHeader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
import {FooterComponent} from './footer/footer.component';
|
||||
import {NavbarComponent} from './navbar/navbar.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
|
@ -10,9 +14,15 @@ describe('AppComponent', () => {
|
|||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
FooterComponent,
|
||||
NavbarComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
RouterTestingModule,
|
||||
]
|
||||
})
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {isSignedIn} from 'sartography-workflow-lib';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -7,4 +8,5 @@ import { Component } from '@angular/core';
|
|||
})
|
||||
export class AppComponent {
|
||||
title = 'CR Connect Configuration';
|
||||
isSignedIn = isSignedIn;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
|
||||
import {Injectable, NgModule} from '@angular/core';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatDialogModule} from '@angular/material/dialog';
|
||||
import {MatDividerModule} from '@angular/material/divider';
|
||||
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from '@angular/material/form-field';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatListModule} from '@angular/material/list';
|
||||
|
@ -18,7 +19,7 @@ import {BrowserModule} from '@angular/platform-browser';
|
|||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {FormlyModule} from '@ngx-formly/core';
|
||||
import {FormlyMaterialModule} from '@ngx-formly/material';
|
||||
import {AppEnvironment} from 'sartography-workflow-lib';
|
||||
import {AppEnvironment, AuthInterceptor, SessionRedirectComponent} from 'sartography-workflow-lib';
|
||||
import {environment} from '../environments/environment';
|
||||
import {DeleteFileDialogComponent} from './_dialogs/delete-file-dialog/delete-file-dialog.component';
|
||||
import {DeleteWorkflowSpecDialogComponent} from './_dialogs/delete-workflow-spec-dialog/delete-workflow-spec-dialog.component';
|
||||
|
@ -26,13 +27,19 @@ import {FileMetaDialogComponent} from './_dialogs/file-meta-dialog/file-meta-dia
|
|||
import {NewFileDialogComponent} from './_dialogs/new-file-dialog/new-file-dialog.component';
|
||||
import {OpenFileDialogComponent} from './_dialogs/open-file-dialog/open-file-dialog.component';
|
||||
import {WorkflowSpecDialogComponent} from './_dialogs/workflow-spec-dialog/workflow-spec-dialog.component';
|
||||
import {EmailValidator, EmailValidatorMessage, ShowError} from './_forms/validators/formly.validator';
|
||||
import {GetIconCodePipe} from './_pipes/get-icon-code.pipe';
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
import {DiagramComponent} from './diagram/diagram.component';
|
||||
import {FileListComponent} from './file-list/file-list.component';
|
||||
import {FooterComponent} from './footer/footer.component';
|
||||
import {ModelerComponent} from './modeler/modeler.component';
|
||||
import {NavbarComponent} from './navbar/navbar.component';
|
||||
import {SignInComponent} from './sign-in/sign-in.component';
|
||||
import {SignOutComponent} from './sign-out/sign-out.component';
|
||||
import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
|
||||
export class ThisEnvironment implements AppEnvironment {
|
||||
production = environment.production;
|
||||
|
@ -41,6 +48,22 @@ export class ThisEnvironment implements AppEnvironment {
|
|||
irbUrl = environment.irbUrl;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppFormlyConfig {
|
||||
public static config = {
|
||||
extras: {
|
||||
showError: ShowError,
|
||||
},
|
||||
validators: [
|
||||
{name: 'email', validation: EmailValidator},
|
||||
],
|
||||
validationMessages: [
|
||||
{name: 'email', message: EmailValidatorMessage},
|
||||
{name: 'required', message: 'This field is required.'},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
@ -49,23 +72,25 @@ export class ThisEnvironment implements AppEnvironment {
|
|||
DiagramComponent,
|
||||
FileListComponent,
|
||||
FileMetaDialogComponent,
|
||||
FooterComponent,
|
||||
GetIconCodePipe,
|
||||
ModelerComponent,
|
||||
NavbarComponent,
|
||||
NewFileDialogComponent,
|
||||
OpenFileDialogComponent,
|
||||
SessionRedirectComponent,
|
||||
SignInComponent,
|
||||
SignOutComponent,
|
||||
WorkflowSpecDialogComponent,
|
||||
WorkflowSpecListComponent,
|
||||
HomeComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FlexLayoutModule,
|
||||
FormlyMaterialModule,
|
||||
FormlyModule.forRoot({
|
||||
validationMessages: [
|
||||
{name: 'required', message: 'This field is required'},
|
||||
],
|
||||
}),
|
||||
FormlyModule.forRoot(AppFormlyConfig.config),
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MatButtonModule,
|
||||
|
@ -92,7 +117,15 @@ export class ThisEnvironment implements AppEnvironment {
|
|||
OpenFileDialogComponent,
|
||||
WorkflowSpecDialogComponent,
|
||||
],
|
||||
providers: [{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment}]
|
||||
providers: [
|
||||
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'fill'}},
|
||||
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AuthInterceptor,
|
||||
multi: true
|
||||
},
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<mat-list>
|
||||
<mat-list-item *ngFor="let fm of fileMetas">
|
||||
<mat-list-item
|
||||
*ngFor="let fm of fileMetas"
|
||||
[attr.data-workflow-spec-id]="workflowSpec.id"
|
||||
[attr.data-file-meta-id]="fm.id"
|
||||
>
|
||||
<mat-icon mat-list-icon (click)="editFile(fm.id)">{{fm.type | getIconCode}}</mat-icon>
|
||||
<ng-container *ngIf="fm.type === fileType.BPMN">
|
||||
<button mat-flat-button *ngIf="!fm.primary" (click)="makePrimary(fm)">
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<footer>
|
||||
CR Connect - University of Virginia
|
||||
</footer>
|
|
@ -0,0 +1,8 @@
|
|||
@import "../../_config.scss";
|
||||
|
||||
footer {
|
||||
background-color: $brand-gray;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
describe('FooterComponent', () => {
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FooterComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<app-sign-in *ngIf="!isSignedIn()"></app-sign-in>
|
||||
<app-workflow-spec-list *ngIf="isSignedIn()"></app-workflow-spec-list>
|
|
@ -0,0 +1,50 @@
|
|||
import {Component, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {MockEnvironment} from 'sartography-workflow-lib';
|
||||
|
||||
import {HomeComponent} from './home.component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-sign-in',
|
||||
template: ''
|
||||
})
|
||||
class MockSignInComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'app-workflow-spec-list',
|
||||
template: ''
|
||||
})
|
||||
class MockWorkflowSpecListComponent {}
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
HomeComponent,
|
||||
MockSignInComponent,
|
||||
MockWorkflowSpecListComponent,
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should check signed-in state', () => {
|
||||
const result = component.isSignedIn();
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toEqual('boolean');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import {Component} from '@angular/core';
|
||||
import {isSignedIn} from 'sartography-workflow-lib';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss']
|
||||
})
|
||||
export class HomeComponent {
|
||||
isSignedIn = isSignedIn;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<nav *ngIf="isSignedIn() && user">
|
||||
<a
|
||||
[routerLink]="['/']"
|
||||
class="site-title mat-display-1"
|
||||
mat-button
|
||||
>CR Connect</a>
|
||||
<div class="flex-spacer"></div>
|
||||
<ng-container *ngFor="let link of navLinks">
|
||||
<a *ngIf="!link.links"
|
||||
[id]="link.id"
|
||||
[ngClass]="{'active': isLinkActive(link.path)}"
|
||||
[routerLink]="link.path"
|
||||
mat-button
|
||||
>
|
||||
{{link.label}}
|
||||
</a>
|
||||
|
||||
<ng-container *ngIf="link.links">
|
||||
<button
|
||||
[attr.aria-label]="link.label"
|
||||
[id]="link.id"
|
||||
[matMenuTriggerFor]="menu"
|
||||
mat-button
|
||||
>
|
||||
<mat-icon>{{link.icon}}</mat-icon>
|
||||
{{link.label}}
|
||||
</button>
|
||||
<mat-menu #menu="matMenu" xPosition="before">
|
||||
<button
|
||||
*ngFor="let menuLink of link.links"
|
||||
[id]="menuLink.id"
|
||||
[ngClass]="{'active': isLinkActive(menuLink.path)}"
|
||||
[routerLink]="menuLink.path"
|
||||
mat-menu-item
|
||||
>
|
||||
<mat-icon *ngIf="menuLink.icon">{{menuLink.icon}}</mat-icon>
|
||||
<span>{{menuLink.label}}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</nav>
|
|
@ -0,0 +1,35 @@
|
|||
@import "../../config";
|
||||
|
||||
.flex-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background-color: $brand-primary;
|
||||
color: white;
|
||||
height: 48px;
|
||||
|
||||
.site-title {
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
line-height: 48px;
|
||||
margin-bottom: 0;
|
||||
background-color: $brand-primary;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
a.active {
|
||||
background-color: $brand-primary-tint-1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mat-menu-item.active {
|
||||
background-color: $brand-gray-tint-2;
|
||||
color: $brand-gray;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {Router} from '@angular/router';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ApiService, MockEnvironment, mockUser} from 'sartography-workflow-lib';
|
||||
|
||||
import { NavbarComponent } from './navbar.component';
|
||||
|
||||
describe('NavbarComponent', () => {
|
||||
let component: NavbarComponent;
|
||||
let fixture: ComponentFixture<NavbarComponent>;
|
||||
let httpMock: HttpTestingController;
|
||||
const mockRouter = {navigate: jasmine.createSpy('navigate')};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
NavbarComponent
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
{
|
||||
provide: Router,
|
||||
useValue: mockRouter
|
||||
},
|
||||
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('token', 'some_token');
|
||||
httpMock = TestBed.get(HttpTestingController);
|
||||
fixture = TestBed.createComponent(NavbarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
const uReq = httpMock.expectOne('apiRoot/user');
|
||||
expect(uReq.request.method).toEqual('GET');
|
||||
uReq.flush(mockUser);
|
||||
expect(component.user).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {ApiService, isSignedIn, User} from 'sartography-workflow-lib';
|
||||
|
||||
interface NavItem {
|
||||
path?: string;
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
links?: NavItem[];
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
templateUrl: './navbar.component.html',
|
||||
styleUrls: ['./navbar.component.scss']
|
||||
})
|
||||
export class NavbarComponent implements OnInit {
|
||||
navLinks: NavItem[];
|
||||
user: User;
|
||||
isSignedIn = isSignedIn;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private api: ApiService,
|
||||
) {
|
||||
this._loadUser();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
isLinkActive(path: string) {
|
||||
return path === this.router.url;
|
||||
}
|
||||
|
||||
private _loadUser() {
|
||||
if (isSignedIn()) {
|
||||
this.api.getUser().subscribe(u => {
|
||||
this.user = u;
|
||||
this._loadNavLinks();
|
||||
}, error => {
|
||||
localStorage.removeItem('token');
|
||||
this.api.openUrl('/');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _loadNavLinks() {
|
||||
const displayName = this.user.display_name || this.user.first_name || this.user.last_name;
|
||||
this.navLinks = [
|
||||
{path: '/', id: 'nav_home', label: 'Home'},
|
||||
{path: '/inbox', id: 'nav_inbox', label: 'Inbox'},
|
||||
{path: '/help', id: 'nav_help', label: 'Help'},
|
||||
{
|
||||
id: 'nav_account', label: `${displayName} (${this.user.email_address})`,
|
||||
icon: 'account_circle',
|
||||
links: [
|
||||
{path: '/profile', id: 'nav_profile', label: 'Profile', icon: 'person'},
|
||||
{path: '/notifications', id: 'nav_notifications', label: 'Notifications', icon: 'notifications'},
|
||||
{path: '/sign-out', id: 'nav_sign_out', label: 'Sign out', icon: 'exit_to_app'},
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<div class="full-height" fxLayout="column" fxLayoutAlign="center center">
|
||||
<h1>Fake UVA NetBadge Sign In (for testing only)</h1>
|
||||
<formly-form [fields]="fields" [form]="form" [model]="model">
|
||||
<mat-error *ngIf="error">{{error}}</mat-error>
|
||||
<button
|
||||
(click)="signIn()"
|
||||
[disabled]="form.invalid"
|
||||
color="primary"
|
||||
id="sign_in"
|
||||
mat-flat-button
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</formly-form>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
form {
|
||||
min-width: 150px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ActivatedRoute, convertToParamMap, Router} from '@angular/router';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {FormlyModule} from '@ngx-formly/core';
|
||||
import {FormlyMaterialModule} from '@ngx-formly/material';
|
||||
import {of} from 'rxjs';
|
||||
import {ApiService, MockEnvironment, mockUser} from 'sartography-workflow-lib';
|
||||
import {EmailValidator, EmailValidatorMessage} from '../_forms/validators/formly.validator';
|
||||
import {SignInComponent} from './sign-in.component';
|
||||
|
||||
describe('SignInComponent', () => {
|
||||
let component: SignInComponent;
|
||||
let fixture: ComponentFixture<SignInComponent>;
|
||||
let httpMock: HttpTestingController;
|
||||
const mockRouter = {navigate: jasmine.createSpy('navigate')};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SignInComponent],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
FormlyModule.forRoot({
|
||||
validators: [
|
||||
{name: 'email', validation: EmailValidator},
|
||||
],
|
||||
validationMessages: [
|
||||
{name: 'email', message: EmailValidatorMessage},
|
||||
],
|
||||
}),
|
||||
FormlyMaterialModule,
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
NoopAnimationsModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {paramMap: of(convertToParamMap({study_id: '0', workflow_id: '0', task_id: '0'}))}
|
||||
},
|
||||
{
|
||||
provide: Router,
|
||||
useValue: mockRouter
|
||||
},
|
||||
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
|
||||
],
|
||||
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock = TestBed.get(HttpTestingController);
|
||||
fixture = TestBed.createComponent(SignInComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should fake sign in during testing', () => {
|
||||
const openSessionSpy = spyOn((component as any).api, 'openSession').and.stub();
|
||||
(component as any).environment.production = false;
|
||||
component.model = mockUser;
|
||||
component.signIn();
|
||||
expect(openSessionSpy).toHaveBeenCalledWith(mockUser);
|
||||
expect(component.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should display an error if sign in is called on production', () => {
|
||||
const openSessionSpy = spyOn((component as any).api, 'openSession').and.stub();
|
||||
(component as any).environment.production = true;
|
||||
component.signIn();
|
||||
expect(openSessionSpy).not.toHaveBeenCalled();
|
||||
expect(component.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should verify the user and redirect to home page on production', () => {
|
||||
const getUserSpy = spyOn((component as any).api, 'getUser').and.returnValue(of(mockUser));
|
||||
(component as any).environment.production = true;
|
||||
(component as any)._redirectOnProduction();
|
||||
expect(getUserSpy).toHaveBeenCalled();
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(['/']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
import {PlatformLocation} from '@angular/common';
|
||||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {FormGroup} from '@angular/forms';
|
||||
import {Router} from '@angular/router';
|
||||
import {FormlyFieldConfig} from '@ngx-formly/core';
|
||||
import {ApiService, AppEnvironment, User} from 'sartography-workflow-lib';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sign-in',
|
||||
templateUrl: './sign-in.component.html',
|
||||
styleUrls: ['./sign-in.component.scss']
|
||||
})
|
||||
export class SignInComponent implements OnInit {
|
||||
form = new FormGroup({});
|
||||
model: any = {};
|
||||
fields: FormlyFieldConfig[] = [
|
||||
{
|
||||
key: 'uid',
|
||||
type: 'input',
|
||||
defaultValue: 'czn1z',
|
||||
templateOptions: {
|
||||
required: true,
|
||||
label: 'UVA Computing ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'email_address',
|
||||
type: 'input',
|
||||
defaultValue: 'czn1z@virginia.edu',
|
||||
templateOptions: {
|
||||
required: true,
|
||||
type: 'email',
|
||||
label: 'UVA Email Address',
|
||||
},
|
||||
validators: {validation: ['email']},
|
||||
},
|
||||
{
|
||||
key: 'first_name',
|
||||
type: 'input',
|
||||
defaultValue: 'Cordi',
|
||||
templateOptions: {
|
||||
label: 'First Name',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'last_name',
|
||||
type: 'input',
|
||||
defaultValue: 'Nator',
|
||||
templateOptions: {
|
||||
label: 'First Name',
|
||||
},
|
||||
},
|
||||
];
|
||||
error: Error;
|
||||
|
||||
|
||||
constructor(
|
||||
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
|
||||
private router: Router,
|
||||
private api: ApiService,
|
||||
private platformLocation: PlatformLocation
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._redirectOnProduction();
|
||||
}
|
||||
|
||||
signIn() {
|
||||
this.error = undefined;
|
||||
localStorage.removeItem('token');
|
||||
|
||||
// For testing purposes, create a user to simulate login.
|
||||
if (!this.environment.production) {
|
||||
this.model.redirect_url = this.platformLocation.href + 'session';
|
||||
this.api.openSession(this.model);
|
||||
} else {
|
||||
this.error = new Error('This feature does not work in production.');
|
||||
}
|
||||
}
|
||||
|
||||
// If this is production, verify the user and redirect to home page.
|
||||
private _redirectOnProduction() {
|
||||
if (this.environment.production) {
|
||||
this.api.getUser().subscribe((user: User) => {
|
||||
this.router.navigate(['/']);
|
||||
}, e => {
|
||||
this.error = e;
|
||||
localStorage.removeItem('token');
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<div class="full-height" fxLayout="column" fxLayoutAlign="center center">
|
||||
<h1>You have been signed out.</h1>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="goHome()">Ok</button>
|
||||
</div>
|
|
@ -0,0 +1,49 @@
|
|||
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {ApiService, MockEnvironment} from 'sartography-workflow-lib';
|
||||
|
||||
import {SignOutComponent} from './sign-out.component';
|
||||
|
||||
describe('SignOutComponent', () => {
|
||||
let component: SignOutComponent;
|
||||
let fixture: ComponentFixture<SignOutComponent>;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SignOutComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock = TestBed.get(HttpTestingController);
|
||||
fixture = TestBed.createComponent(SignOutComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should go home', () => {
|
||||
const openUrlSpy = spyOn((component as any).api, 'openUrl').and.stub();
|
||||
component.goHome();
|
||||
expect(openUrlSpy).toHaveBeenCalledWith('/');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {ApiService} from 'sartography-workflow-lib';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sign-out',
|
||||
templateUrl: './sign-out.component.html',
|
||||
styleUrls: ['./sign-out.component.scss']
|
||||
})
|
||||
export class SignOutComponent implements OnInit {
|
||||
|
||||
constructor(private api: ApiService) {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
goHome() {
|
||||
this.api.openUrl('/');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
@import "material";
|
||||
|
||||
@include mat-core($custom-typography);
|
||||
@include angular-material-theme($cr-connect-theme);
|
||||
|
||||
@mixin cr-connect-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
|
||||
h1 {
|
||||
text-transform: uppercase;
|
||||
color: $brand-accent;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
mat-radio-button, .mat-checkbox {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
formly-field mat-form-field {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
formly-field.textarea-cols {
|
||||
display: flow-root;
|
||||
|
||||
& > formly-wrapper-mat-form-field > mat-form-field {
|
||||
width: auto !important;
|
||||
|
||||
.mat-form-field-infix {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formly-field.read-only {
|
||||
.mat-form-field-outline {
|
||||
background-color: $brand-gray-tint-2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
formly-field.vertical-radio-group {
|
||||
mat-radio-button {
|
||||
margin: 5px;
|
||||
padding-right: 16px;
|
||||
display: block;
|
||||
|
||||
label.mat-radio-label {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formly-field.vertical-checkbox-group {
|
||||
.mat-checkbox {
|
||||
margin: 5px;
|
||||
padding-right: 16px;
|
||||
display: block;
|
||||
|
||||
.mat-checkbox-layout {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-height {
|
||||
width: 100%;
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.container, .row {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
//.container {
|
||||
// display: grid;
|
||||
// justify-content: center;
|
||||
// justify-items: center;
|
||||
// grid-template-columns: 1fr;
|
||||
// position: relative;
|
||||
// max-width: 100vw;
|
||||
//
|
||||
// .row {
|
||||
// margin-top: 4em;
|
||||
// margin-bottom: 4em;
|
||||
// position: relative;
|
||||
//
|
||||
// max-width: 100vw;
|
||||
// @media (min-width: 576px) {
|
||||
// max-width: calc(100% - 40px);
|
||||
// }
|
||||
// @media (min-width: 768px) {
|
||||
// max-width: calc(100% - 80px);
|
||||
// }
|
||||
// @media (min-width: 992px) {
|
||||
// max-width: calc(100% - 100px);
|
||||
// }
|
||||
// @media (min-width: 1200px) {
|
||||
// max-width: calc(100% - 120px);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
button {
|
||||
&.btn-xl {
|
||||
font-size: 24px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
&.btn-lg {
|
||||
font-size: 20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: $brand-gray !important;
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field.mat-form-field {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mat-form-field-wrapper .mat-form-field-subscript-wrapper {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2em;
|
||||
|
||||
&.alert-info {
|
||||
background-color: $brand-primary-light;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.alert-warn {
|
||||
background-color: $brand-warning;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-drawer .mat-nav-list .mat-list-item {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.mat-drawer .mat-nav-list .mat-list-item.active {
|
||||
background-color: $brand-primary;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pad-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
.pad-1 {
|
||||
padding: 1em;
|
||||
}
|
||||
.pad-2 {
|
||||
padding: 2em;
|
||||
}
|
||||
.pad-3 {
|
||||
padding: 3em;
|
||||
}
|
||||
.pad-4 {
|
||||
padding: 4em;
|
||||
}
|
||||
.pad-5 {
|
||||
padding: 5em;
|
||||
}
|
||||
.pad-6 {
|
||||
padding: 6em;
|
||||
}
|
||||
.pad-7 {
|
||||
padding: 7em;
|
||||
}
|
||||
.pad-8 {
|
||||
padding: 8em;
|
||||
}
|
||||
|
||||
.margin-top-none, .row.margin-top-none {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
.margin-bottom-none, .row.margin-bottom-none {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
.ghost {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// XS
|
||||
@media (max-width: 575px) {
|
||||
.cdk-overlay-wrapper .cdk-overlay-pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
mat-dialog-container.mat-dialog-container {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// SM
|
||||
@media (min-width: 576px) {
|
||||
.cdk-overlay-wrapper .cdk-overlay-pane {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
mat-dialog-container.mat-dialog-container {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
// MD
|
||||
@media (min-width: 768px) {
|
||||
.cdk-overlay-wrapper .cdk-overlay-pane {
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
}
|
||||
|
||||
mat-dialog-container.mat-dialog-container {
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
// LG
|
||||
@media (min-width: 992px) {
|
||||
.cdk-overlay-wrapper .cdk-overlay-pane {
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
}
|
||||
|
||||
mat-dialog-container.mat-dialog-container {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
// XL
|
||||
@media (min-width: 1200px) {
|
||||
.cdk-overlay-wrapper .cdk-overlay-pane {
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
}
|
||||
|
||||
mat-dialog-container.mat-dialog-container {
|
||||
width: 70vw;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-dialog-content[mat-dialog-content] {
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.loading {
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,6 @@
|
|||
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
@import '~bpmn-js/dist/assets/diagram-js.css';
|
||||
@import '~bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
|
||||
@import '~bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css';
|
||||
@import '~diagram-js-minimap/assets/diagram-js-minimap.css';
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.container, .row {
|
||||
padding: 1em;
|
||||
}
|
||||
@import './material-theme.scss';
|
||||
@include cr-connect-theme($cr-connect-theme);
|
Loading…
Reference in New Issue