Merge pull request #5 from sartography/feature/fake-sso

Feature/fake sso
This commit is contained in:
Aaron Louie 2020-02-25 09:37:33 -05:00 committed by GitHub
commit 437e76a007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1586 additions and 45 deletions

View File

@ -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": [

View File

@ -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');

View 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);
}
}

6
package-lock.json generated
View File

@ -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"
}

View File

@ -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"

67
src/_config.scss Normal file
View File

@ -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;

92
src/_material.scss Normal file
View File

@ -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);

View File

@ -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;

View File

@ -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();
}
});
});

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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)
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -1,21 +1,49 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {NgModule} from '@angular/core';
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 {
}

View File

@ -1 +1,5 @@
<router-outlet></router-outlet>
<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>

View File

@ -0,0 +1,7 @@
#globalHeader {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 2;
}

View File

@ -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,
]
})

View File

@ -1,4 +1,5 @@
import { Component } from '@angular/core';
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;
}

View File

@ -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 {
}

View File

@ -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)">

View File

@ -0,0 +1,3 @@
<footer>
CR Connect - University of Virginia
</footer>

View File

@ -0,0 +1,8 @@
@import "../../_config.scss";
footer {
background-color: $brand-gray;
color: white;
text-align: center;
padding: 20px;
}

View File

@ -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();
});
});

View File

@ -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() {
}
}

View File

@ -0,0 +1,2 @@
<app-sign-in *ngIf="!isSignedIn()"></app-sign-in>
<app-workflow-spec-list *ngIf="isSignedIn()"></app-workflow-spec-list>

View File

View File

@ -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');
});
});

View File

@ -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() {
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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'},
]
}
];
}
}

View File

@ -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>

View File

@ -0,0 +1,9 @@
form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.full-width {
width: 100%;
}

View File

@ -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(['/']);
});
});

View File

@ -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');
}
}
}

View File

@ -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>

View File

View File

@ -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('/');
});
});

View File

@ -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('/');
}
}

278
src/material-theme.scss Normal file
View File

@ -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);
}
}

View File

@ -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);