initial relocation.
This commit is contained in:
Родитель
8dbecbd4f8
Коммит
f31a35597d
|
@ -0,0 +1,13 @@
|
|||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,7 @@
|
|||
################################################################################
|
||||
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
|
||||
################################################################################
|
||||
|
||||
/node_modules/
|
||||
/.vs/
|
||||
notcheckedin/
|
62
README.md
62
README.md
|
@ -1,3 +1,65 @@
|
|||
<!--
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
Licensed under the MIT License.
|
||||
-->
|
||||
|
||||
## Standards Mapping
|
||||
|
||||
Standards Mapping is an open source tool used to map and visualize standards and regulations.
|
||||
|
||||
### Running the app
|
||||
|
||||
You can access the app live at the following URL: https://mkslalom.github.io/standards-mapping/.
|
||||
|
||||
### Building the code
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
Please ensure that you have at least the **minimum** recommended versions
|
||||
|
||||
- Node >= 9.0.0
|
||||
|
||||
#### 1. Clone the repository
|
||||
|
||||
- Clone the repository using one of the following commands
|
||||
```bash
|
||||
git clone https://github.com/mkslalom/standards-mapping.git
|
||||
```
|
||||
or
|
||||
```bash
|
||||
git clone git@github.com:mkslalom/standards-mapping.git
|
||||
```
|
||||
- Select the created directory
|
||||
```bash
|
||||
cd standards-mapping
|
||||
```
|
||||
|
||||
#### 2. Install packages
|
||||
|
||||
- Install the packages
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
(Temporary note from Matt. You may need to install Angular CLI separately: npm install -g @angular/cli)
|
||||
|
||||
#### 3. Build and run
|
||||
|
||||
- Run the dev server
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
#### 4. Open app in web browser
|
||||
|
||||
- Navigate your browser to:
|
||||
http://localhost:4200/dashboard
|
||||
|
||||
### More Information
|
||||
|
||||
Please contact: Tom Wagner <tomw@slalom.com>, Matt Kincaid <matt.kincaid@slalom.com>, Douglas Branca <dougb@slalom.com>
|
||||
|
||||
# data-protection-mapping-project
|
||||
Open Source Data Protection/Privacy Regulatory Mapping Project
|
||||
Copyright <2020> <Data Protection Mapping Project>
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular.io-example": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"schematics": {},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"node_modules/angular-tree-component/dist/angular-tree-component.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "angular.io-example:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "angular.io-example:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "angular.io-example:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"angular.io-example-e2e": {
|
||||
"root": "e2e/",
|
||||
"projectType": "application",
|
||||
"prefix": "",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "angular.io-example:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "angular.io-example:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "angular.io-example"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome',
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,307 @@
|
|||
'use strict'; // necessary for es6 output in node
|
||||
|
||||
import { browser, element, by, ElementFinder, ElementArrayFinder } from 'protractor';
|
||||
import { promise } from 'selenium-webdriver';
|
||||
|
||||
const expectedH1 = 'Tour of Heroes';
|
||||
const expectedTitle = `${expectedH1}`;
|
||||
const targetHero = { id: 15, name: 'Magneta' };
|
||||
const targetHeroDashboardIndex = 3;
|
||||
const nameSuffix = 'X';
|
||||
const newHeroName = targetHero.name + nameSuffix;
|
||||
|
||||
class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
// Factory methods
|
||||
|
||||
// Hero from string formatted as '<id> <name>'.
|
||||
static fromString(s: string): Hero {
|
||||
return {
|
||||
id: +s.substr(0, s.indexOf(' ')),
|
||||
name: s.substr(s.indexOf(' ') + 1),
|
||||
};
|
||||
}
|
||||
|
||||
// Hero from hero list <li> element.
|
||||
static async fromLi(li: ElementFinder): Promise<Hero> {
|
||||
let stringsFromA = await li.all(by.css('a')).getText();
|
||||
let strings = stringsFromA[0].split(' ');
|
||||
return { id: +strings[0], name: strings[1] };
|
||||
}
|
||||
|
||||
// Hero id and name from the given detail element.
|
||||
static async fromDetail(detail: ElementFinder): Promise<Hero> {
|
||||
// Get hero id from the first <div>
|
||||
let _id = await detail.all(by.css('div')).first().getText();
|
||||
// Get name from the h2
|
||||
let _name = await detail.element(by.css('h2')).getText();
|
||||
return {
|
||||
id: +_id.substr(_id.indexOf(' ') + 1),
|
||||
name: _name.substr(0, _name.lastIndexOf(' '))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('Tutorial part 6', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
function getPageElts() {
|
||||
let navElts = element.all(by.css('app-root nav a'));
|
||||
|
||||
return {
|
||||
navElts: navElts,
|
||||
|
||||
appDashboardHref: navElts.get(0),
|
||||
appDashboard: element(by.css('app-root app-dashboard')),
|
||||
topHeroes: element.all(by.css('app-root app-dashboard > div h4')),
|
||||
|
||||
appHeroesHref: navElts.get(1),
|
||||
appHeroes: element(by.css('app-root app-heroes')),
|
||||
allHeroes: element.all(by.css('app-root app-heroes li')),
|
||||
selectedHeroSubview: element(by.css('app-root app-heroes > div:last-child')),
|
||||
|
||||
heroDetail: element(by.css('app-root app-hero-detail > div')),
|
||||
|
||||
searchBox: element(by.css('#search-box')),
|
||||
searchResults: element.all(by.css('.search-result li'))
|
||||
};
|
||||
}
|
||||
|
||||
describe('Initial page', () => {
|
||||
|
||||
it(`has title '${expectedTitle}'`, () => {
|
||||
expect(browser.getTitle()).toEqual(expectedTitle);
|
||||
});
|
||||
|
||||
it(`has h1 '${expectedH1}'`, () => {
|
||||
expectHeading(1, expectedH1);
|
||||
});
|
||||
|
||||
const expectedViewNames = ['Dashboard', 'Heroes'];
|
||||
it(`has views ${expectedViewNames}`, () => {
|
||||
let viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText());
|
||||
expect(viewNames).toEqual(expectedViewNames);
|
||||
});
|
||||
|
||||
it('has dashboard as the active view', () => {
|
||||
let page = getPageElts();
|
||||
expect(page.appDashboard.isPresent()).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Dashboard tests', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it('has top heroes', () => {
|
||||
let page = getPageElts();
|
||||
expect(page.topHeroes.count()).toEqual(4);
|
||||
});
|
||||
|
||||
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`cancels and shows ${targetHero.name} in Dashboard`, () => {
|
||||
element(by.buttonText('go back')).click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||
|
||||
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||
});
|
||||
|
||||
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`saves and shows ${newHeroName} in Dashboard`, () => {
|
||||
element(by.buttonText('save')).click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||
|
||||
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(newHeroName);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Heroes tests', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it('can switch to Heroes view', () => {
|
||||
getPageElts().appHeroesHref.click();
|
||||
let page = getPageElts();
|
||||
expect(page.appHeroes.isPresent()).toBeTruthy();
|
||||
expect(page.allHeroes.count()).toEqual(10, 'number of heroes');
|
||||
});
|
||||
|
||||
it('can route to hero details', async () => {
|
||||
getHeroLiEltById(targetHero.id).click();
|
||||
|
||||
let page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
let hero = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero.id).toEqual(targetHero.id);
|
||||
expect(hero.name).toEqual(targetHero.name.toUpperCase());
|
||||
});
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`shows ${newHeroName} in Heroes list`, () => {
|
||||
element(by.buttonText('save')).click();
|
||||
browser.waitForAngular();
|
||||
let expectedText = `${targetHero.id} ${newHeroName}`;
|
||||
expect(getHeroAEltById(targetHero.id).getText()).toEqual(expectedText);
|
||||
});
|
||||
|
||||
it(`deletes ${newHeroName} from Heroes list`, async () => {
|
||||
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||
const li = getHeroLiEltById(targetHero.id);
|
||||
li.element(by.buttonText('x')).click();
|
||||
|
||||
const page = getPageElts();
|
||||
expect(page.appHeroes.isPresent()).toBeTruthy();
|
||||
expect(page.allHeroes.count()).toEqual(9, 'number of heroes');
|
||||
const heroesAfter = await toHeroArray(page.allHeroes);
|
||||
// console.log(await Hero.fromLi(page.allHeroes[0]));
|
||||
const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName);
|
||||
expect(heroesAfter).toEqual(expectedHeroes);
|
||||
// expect(page.selectedHeroSubview.isPresent()).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`adds back ${targetHero.name}`, async () => {
|
||||
const newHeroName = 'Alice';
|
||||
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||
const numHeroes = heroesBefore.length;
|
||||
|
||||
element(by.css('input')).sendKeys(newHeroName);
|
||||
element(by.buttonText('add')).click();
|
||||
|
||||
let page = getPageElts();
|
||||
let heroesAfter = await toHeroArray(page.allHeroes);
|
||||
expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes');
|
||||
|
||||
expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there');
|
||||
|
||||
const maxId = heroesBefore[heroesBefore.length - 1].id;
|
||||
expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: newHeroName});
|
||||
});
|
||||
|
||||
it('displays correctly styled buttons', async () => {
|
||||
element.all(by.buttonText('x')).then(buttons => {
|
||||
for (const button of buttons) {
|
||||
// Inherited styles from styles.css
|
||||
expect(button.getCssValue('font-family')).toBe('Arial');
|
||||
expect(button.getCssValue('border')).toContain('none');
|
||||
expect(button.getCssValue('padding')).toBe('5px 10px');
|
||||
expect(button.getCssValue('border-radius')).toBe('4px');
|
||||
// Styles defined in heroes.component.css
|
||||
expect(button.getCssValue('left')).toBe('194px');
|
||||
expect(button.getCssValue('top')).toBe('-32px');
|
||||
}
|
||||
});
|
||||
|
||||
const addButton = element(by.buttonText('add'));
|
||||
// Inherited styles from styles.css
|
||||
expect(addButton.getCssValue('font-family')).toBe('Arial');
|
||||
expect(addButton.getCssValue('border')).toContain('none');
|
||||
expect(addButton.getCssValue('padding')).toBe('5px 10px');
|
||||
expect(addButton.getCssValue('border-radius')).toBe('4px');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Progressive hero search', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it(`searches for 'Ma'`, async () => {
|
||||
getPageElts().searchBox.sendKeys('Ma');
|
||||
browser.sleep(1000);
|
||||
|
||||
expect(getPageElts().searchResults.count()).toBe(4);
|
||||
});
|
||||
|
||||
it(`continues search with 'g'`, async () => {
|
||||
getPageElts().searchBox.sendKeys('g');
|
||||
browser.sleep(1000);
|
||||
expect(getPageElts().searchResults.count()).toBe(2);
|
||||
});
|
||||
|
||||
it(`continues search with 'e' and gets ${targetHero.name}`, async () => {
|
||||
getPageElts().searchBox.sendKeys('n');
|
||||
browser.sleep(1000);
|
||||
let page = getPageElts();
|
||||
expect(page.searchResults.count()).toBe(1);
|
||||
let hero = page.searchResults.get(0);
|
||||
expect(hero.getText()).toEqual(targetHero.name);
|
||||
});
|
||||
|
||||
it(`navigates to ${targetHero.name} details view`, async () => {
|
||||
let hero = getPageElts().searchResults.get(0);
|
||||
expect(hero.getText()).toEqual(targetHero.name);
|
||||
hero.click();
|
||||
|
||||
let page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
let hero2 = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero2.id).toEqual(targetHero.id);
|
||||
expect(hero2.name).toEqual(targetHero.name.toUpperCase());
|
||||
});
|
||||
});
|
||||
|
||||
async function dashboardSelectTargetHero() {
|
||||
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||
targetHeroElt.click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
|
||||
|
||||
let page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
let hero = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero.id).toEqual(targetHero.id);
|
||||
expect(hero.name).toEqual(targetHero.name.toUpperCase());
|
||||
}
|
||||
|
||||
async function updateHeroNameInDetailView() {
|
||||
// Assumes that the current view is the hero details view.
|
||||
addToHeroName(nameSuffix);
|
||||
|
||||
let page = getPageElts();
|
||||
let hero = await Hero.fromDetail(page.heroDetail);
|
||||
expect(hero.id).toEqual(targetHero.id);
|
||||
expect(hero.name).toEqual(newHeroName.toUpperCase());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function addToHeroName(text: string): promise.Promise<void> {
|
||||
let input = element(by.css('input'));
|
||||
return input.sendKeys(text);
|
||||
}
|
||||
|
||||
function expectHeading(hLevel: number, expectedText: string): void {
|
||||
let hTag = `h${hLevel}`;
|
||||
let hText = element(by.css(hTag)).getText();
|
||||
expect(hText).toEqual(expectedText, hTag);
|
||||
};
|
||||
|
||||
function getHeroAEltById(id: number): ElementFinder {
|
||||
let spanForId = element(by.cssContainingText('li span.badge', id.toString()));
|
||||
return spanForId.element(by.xpath('..'));
|
||||
}
|
||||
|
||||
function getHeroLiEltById(id: number): ElementFinder {
|
||||
let spanForId = element(by.cssContainingText('li span.badge', id.toString()));
|
||||
return spanForId.element(by.xpath('../..'));
|
||||
}
|
||||
|
||||
async function toHeroArray(allHeroes: ElementArrayFinder): Promise<Hero[]> {
|
||||
let promisedHeroes = await allHeroes.map(Hero.fromLi);
|
||||
// The cast is necessary to get around issuing with the signature of Promise.all()
|
||||
return <Promise<any>> Promise.all(promisedHeroes);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"name": "angular-io-example",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Example project from an angular.io guide.",
|
||||
"scripts": {
|
||||
"lint": "tslint ./src/**/*.ts -t verbose",
|
||||
"start": "ng serve",
|
||||
"test": "ng test",
|
||||
"build": "ng build --aot=true --prod=true --outputPath=docs",
|
||||
"e2e": "ng e2e",
|
||||
"ng": "ng",
|
||||
"import": "node utils/importer.js",
|
||||
"convert": "node utils/converter.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^7.1.0",
|
||||
"@angular/cdk": "~7.3.7",
|
||||
"@angular/common": "^7.1.0",
|
||||
"@angular/compiler": "^7.1.0",
|
||||
"@angular/core": "^7.1.0",
|
||||
"@angular/flex-layout": "^7.0.0-beta.24",
|
||||
"@angular/forms": "^7.1.0",
|
||||
"@angular/http": "^7.1.0",
|
||||
"@angular/material": "^7.3.7",
|
||||
"@angular/platform-browser": "^7.1.0",
|
||||
"@angular/platform-browser-dynamic": "^7.1.0",
|
||||
"@angular/router": "^7.1.0",
|
||||
"@angular/upgrade": "^7.1.0",
|
||||
"@types/d3-sankey": "^0.11.0",
|
||||
"angular-in-memory-web-api": "github:brandonroberts/in-memory-web-api-bazel#50a34d8",
|
||||
"angular-tree-component": "^8.3.0",
|
||||
"color-convert": "^2.0.0",
|
||||
"core-js": "^2.5.4",
|
||||
"d3": "^5.9.2",
|
||||
"d3-ng2-service": "^2.2.0",
|
||||
"d3-sankey": "^0.12.1",
|
||||
"d3-selection-multi": "^1.0.1",
|
||||
"exceljs": "^1.11.0",
|
||||
"fuse.js": "^3.4.5",
|
||||
"rxjs": "^6.5.1",
|
||||
"zone.js": "~0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.11.0",
|
||||
"@angular/cli": "^7.1.0",
|
||||
"@angular/compiler-cli": "^7.1.0",
|
||||
"@angular/platform-server": "^7.1.0",
|
||||
"@types/jasmine": "~2.8.8",
|
||||
"@types/jasminewd2": "^2.0.4",
|
||||
"@types/node": "~8.9.4",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~3.0.0",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "~1.1.2",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"lodash": "^4.16.2",
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"typescript": "~3.1.1"
|
||||
},
|
||||
"repository": {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { StandardMapsComponent } from './standard-maps/standard-maps.component';
|
||||
import { StandardMapDetailComponent } from './standard-map-detail/standard-map-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'doctypes/:id', component: StandardMapDetailComponent },
|
||||
{ path: 'standard-map', component: StandardMapsComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [ RouterModule.forRoot(routes, { useHash:true }) ],
|
||||
exports: [ RouterModule ]
|
||||
})
|
||||
export class AppRoutingModule {}
|
|
@ -0,0 +1,41 @@
|
|||
.example-icon {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.example-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.app-body {
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 64px;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
mat-sidenav-container {
|
||||
width: 100% !important;
|
||||
height: calc(100vh - 64px) !important;
|
||||
}
|
||||
|
||||
mat-sidenav {
|
||||
background-color: red !important;
|
||||
}
|
||||
|
||||
|
||||
mat-toolbar {
|
||||
height: 64px;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<div class="mat-app-background basic-container">
|
||||
<mat-toolbar class="mat-elevation-z6" color="primary">
|
||||
<mat-toolbar-row>
|
||||
<span>{{title}}</span>
|
||||
<span class="example-spacer"></span>
|
||||
<a href="https://github.com/mkslalom/standards-mapping"><img src="assets/icons/github-white.svg" /></a>
|
||||
</mat-toolbar-row>
|
||||
</mat-toolbar>
|
||||
<mat-sidenav-container>
|
||||
<mat-sidenav mode="side"
|
||||
opened="false"
|
||||
[fixedInViewport]="true"
|
||||
[fixedTopGap]="64">
|
||||
sidenav content
|
||||
</mat-sidenav>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-sidenav-container>
|
||||
|
||||
|
||||
<!--< nav>
|
||||
<a routerLink="/dashboard">Dashboard</a>
|
||||
<a routerLink="/standard-map">Standard Maps</a>
|
||||
</!--nav>-->
|
||||
<!--<app-messages></app-messages>-->
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'Standard Maps';
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
|
||||
import { InMemoryDataService } from './in-memory-data.service';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { StandardMapDetailComponent } from './standard-map-detail/standard-map-detail.component';
|
||||
import { StandardMapsComponent } from './standard-maps/standard-maps.component';
|
||||
import { StandardMapSearchComponent } from './standard-map-search/standard-map-search.component';
|
||||
import { MessagesComponent } from './messages/messages.component';
|
||||
import { D3TestComponent } from './d3-test/d3-test.component';
|
||||
import { TreeModule } from 'angular-tree-component';
|
||||
|
||||
import {
|
||||
MatAutocompleteModule,
|
||||
MatBadgeModule,
|
||||
MatBottomSheetModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatDividerModule,
|
||||
MatExpansionModule,
|
||||
MatGridListModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatMenuModule,
|
||||
MatNativeDateModule,
|
||||
MatPaginatorModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatRadioModule,
|
||||
MatRippleModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatSnackBarModule,
|
||||
MatSortModule,
|
||||
MatStepperModule,
|
||||
MatTableModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
MatTreeModule,
|
||||
} from '@angular/material';
|
||||
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
|
||||
//// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
|
||||
//// and returns simulated server responses.
|
||||
//// Remove it when a real server is ready to receive requests.
|
||||
//HttpClientInMemoryWebApiModule.forRoot(
|
||||
// InMemoryDataService, { dataEncapsulation: false }
|
||||
//),
|
||||
TreeModule.forRoot(),
|
||||
|
||||
// Material modules
|
||||
MatAutocompleteModule,
|
||||
MatBadgeModule,
|
||||
MatBottomSheetModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCardModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatStepperModule,
|
||||
MatDatepickerModule,
|
||||
MatDialogModule,
|
||||
MatDividerModule,
|
||||
MatExpansionModule,
|
||||
MatGridListModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatListModule,
|
||||
MatMenuModule,
|
||||
MatNativeDateModule,
|
||||
MatPaginatorModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatRadioModule,
|
||||
MatRippleModule,
|
||||
MatSelectModule,
|
||||
MatSidenavModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatSnackBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
MatTabsModule,
|
||||
MatToolbarModule,
|
||||
MatTooltipModule,
|
||||
MatTreeModule,
|
||||
BrowserAnimationsModule,
|
||||
FlexLayoutModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
DashboardComponent,
|
||||
StandardMapsComponent,
|
||||
StandardMapDetailComponent,
|
||||
MessagesComponent,
|
||||
StandardMapSearchComponent,
|
||||
D3TestComponent
|
||||
],
|
||||
//providers: [D3Service],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
|
@ -0,0 +1,176 @@
|
|||
|
||||
.container {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.2em;
|
||||
color: #999;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.node {
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: #999;
|
||||
stroke-opacity: .6;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graph-categories {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-content-wrapper {
|
||||
border: 1px solid #d6d6d6;
|
||||
width: 100%;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
.d3 {
|
||||
display: inline-block;
|
||||
width: 74%;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
table-layout: fixed;
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.graph-tab-buttons {
|
||||
|
||||
}
|
||||
|
||||
.graph-tab-button {
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.button-active {
|
||||
border: 1px solid green;
|
||||
}
|
||||
|
||||
.graph-tabs {
|
||||
|
||||
}
|
||||
|
||||
.graph-tab {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.graph-inactive {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.graph-columns {
|
||||
height: 680px;
|
||||
}
|
||||
|
||||
.graph-column {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 60px;
|
||||
max-width: 500px;
|
||||
height: 600px;
|
||||
padding-bottom: 20px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.example-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
height: 482px;
|
||||
}
|
||||
|
||||
tree-viewport {
|
||||
height: calc(100% + 32px) !important;
|
||||
}
|
||||
|
||||
.button-disabled {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.svg-background {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
top: 60px;
|
||||
height: calc(100% - 80px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.svg-line-click {
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
stroke-width: 20px;
|
||||
opacity: .0;
|
||||
}
|
||||
|
||||
.svg-line-visible {
|
||||
stroke: #b7b5b5;
|
||||
}
|
||||
|
||||
.svg-line-active {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.mat-tab-label {
|
||||
min-width: 50px !important;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
width: 100% !important;
|
||||
height: calc(100% - 32px) !important;
|
||||
}
|
||||
|
||||
.mat-tab-body-content {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
mat-checkbox {
|
||||
margin-left: 10px !important;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fake-link {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
text-underline-position: under;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.coverage-box {
|
||||
padding: 10px;
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
<div (window:resize)="onResize($event)">
|
||||
<!-- <div class="graph-type">
|
||||
<input name="options" type="radio" [(ngModel)]="graphType" [value]="0"
|
||||
[checked]="graphType==0" (change)="RefreshGraph()" />
|
||||
<input name="options" type="radio" [(ngModel)]="graphType" [value]="1"
|
||||
[checked]="graphType==1" (change)="RefreshGraph()"/>
|
||||
<input name="options" type="radio" [(ngModel)]="graphType" [value]="2"
|
||||
[checked]="graphType==2" (change)="RefreshGraph()"/>
|
||||
</div>-->
|
||||
<div fxLayout="row" fxLayoutGap="32px" fxLayoutAlign="flex-start" class="container">
|
||||
<div fxFlex="0 0 300px">
|
||||
<mat-card class="graph-categories">
|
||||
<div class="title-container">
|
||||
<mat-card-title class="filter-title">Filter</mat-card-title>
|
||||
<span class="example-spacer"></span>
|
||||
<button mat-button [matMenuTriggerFor]="menu" class="add-button" [class.button-disabled]="!graphService.canAdd"><mat-icon>add_box</mat-icon></button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let d of getMenuOptions()" (click)="graphService.addTab(d.id)">{{d.id}}</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<div class="graph-tabs">
|
||||
<mat-tab-group [(selectedIndex)]="graphService.selectedTab" (selectedTabChange)="tabChanged()">
|
||||
<mat-tab *ngFor="let t of graphService.graphTabs">
|
||||
<ng-template mat-tab-label>
|
||||
{{t.title}}
|
||||
<button mat-button *ngIf="t.title != 'ISO'" matSuffix mat-icon-button aria-label="Clear" (click)="graphService.removeTab(t)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
<div class="coverage-box" *ngIf="!t.isIso"><a class="fake-link" (click)="filterIsoCoverage(t)">ISO:</a> {{t.coverage.coverage}}, <a class="fake-link" (click)="filterMapped(t)">Mapped:</a> {{t.coverage.mapped}}, <span>Unique:</span> {{t.coverage.uniqueconnections}}</div>
|
||||
<!--<div class="coverage-box" *ngIf="t.isIso">-</div>-->
|
||||
<div>
|
||||
<mat-form-field class="filter-field">
|
||||
<input matInput type="text" placeholder="Search..." [(ngModel)]="t.searchValue" #filter3 (keyup)="filter(filter3.value, tree.treeModel)">
|
||||
<button mat-button *ngIf="t.searchValue" matSuffix mat-icon-button aria-label="Clear" (click)="filter('', tree.treeModel); t.searchValue=''">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox *ngIf="t.anyExpanded" (change)="t.expandAll()" [checked]="true">Collapse All</mat-checkbox>
|
||||
<mat-checkbox *ngIf="!t.anyExpanded" (change)="t.expandAll()" [checked]="false">Expand All</mat-checkbox>
|
||||
<mat-checkbox *ngIf="t.anySelected" (change)="t.selectAll()" [checked]="true">Deselect All</mat-checkbox>
|
||||
<mat-checkbox *ngIf="!t.anySelected" (change)="t.selectAll()" [checked]="false">Select All</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="tree-container">
|
||||
<tree-root #tree [(state)]="t.state" [options]="t.options" [nodes]="t.nodes" (initialized)="t.treeModel = tree.treeModel" (stateChange)="t.parentTabTreeChanged(updateSubject)">
|
||||
<ng-template #treeNodeWrapperTemplate let-node let-index="index">
|
||||
<div #wrapper class="node-wrapper" [style.padding-left]="node.getNodePadding()">
|
||||
<tree-node-expander [node]="node"></tree-node-expander>
|
||||
<tree-node-checkbox [node]="node"></tree-node-checkbox>
|
||||
<div class="node-content-wrapper"
|
||||
[class.dummystyle]="bindTogether(node, wrapper, svgbg)"
|
||||
[class.node-content-wrapper-active]="node.isActive"
|
||||
[class.node-content-wrapper-focused]="node.isFocused"
|
||||
(click)="node.mouseAction('click', $event)"
|
||||
[style.background-color]="getNodeColor(t, node)">
|
||||
<span>
|
||||
<span *ngIf="!node.data.node.hyperlink" [innerHTML]="injectHighlightSection(node.data)"></span>
|
||||
<a *ngIf="node.data.node.hyperlink" (mousedown)="openTab(node.data.node.hyperlink)" class="fake-link" target="_blank" href="{{node.data.node.hyperlink}}" [innerHTML]="injectHighlightSection(node.data)"></a>
|
||||
<span [innerHTML]="injectHighlightBody(node.data)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</tree-root>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div fxFlex="1 0 auto">
|
||||
<mat-card class="graph-columns" >
|
||||
<!-- <div fxLayout="row" fxLayoutGap="80px" fxLayoutAlign="flex-start">-->
|
||||
<div class="graph-column" *ngFor="let t of graphService.graphTabs" >
|
||||
<h3>{{t.title}}</h3>
|
||||
<!-- <div><a routerLink="/doctypes/{{t.title}}"><h3>{{t.title}}</h3></a>
|
||||
<div>
|
||||
<mat-checkbox (change)="t.column.expandAll()" [checked]="t.column.anyExpanded">{{t.column.anyExpanded ? 'Collapse' : 'Expand'}} All</mat-checkbox>
|
||||
</div></div>-->
|
||||
|
||||
<tree-root #tree [(state)]="t.column.state" [options]="t.column.options" [nodes]="t.column.nodes" (initialized)="setup(t.column, tree); t.parentTabTreeChanged(updateSubject)" (activate)="activateNode(t, $event)" (deactivate)="t.columnTabTreeChanged(null, updateSubject)" (toggleExpanded)="t.columnTabTreeChanged($event, updateSubject)">
|
||||
<ng-template #treeNodeWrapperTemplate let-node let-index="index">
|
||||
<div #wrapper class="node-wrapper" [style.padding-left]="node.getNodePadding()">
|
||||
<!--<tree-node-expander [node]="node"></tree-node-expander>-->
|
||||
<div class="node-content-wrapper"
|
||||
[class.dummystyle]="bindTogether(node, wrapper, svgbg)"
|
||||
[class.node-content-wrapper-active]="node.isActive"
|
||||
[class.node-content-wrapper-focused]="node.isFocused"
|
||||
(click)="node.mouseAction('click', $event)"
|
||||
[style.background-color]="getNodeColor(t.column, node)">
|
||||
|
||||
<span>
|
||||
<span *ngIf="!node.data.node.hyperlink">{{node.data.node.section}}</span>
|
||||
<a *ngIf="node.data.node.hyperlink" (mousedown)="openTab(node.data.node.hyperlink)" class="fake-link" target="_blank" href="{{node.data.node.hyperlink}}">{{node.data.node.section}}</a>
|
||||
<span>{{node.data.node.body ? (' - ' + node.data.node.body) : ''}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</tree-root>
|
||||
</div>
|
||||
<!-- </div>-->
|
||||
<svg #svgbg class="svg-background">
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
|
||||
<path d="M0,0 L0,6 L9,3 z" fill="#000" />
|
||||
</marker>
|
||||
</defs>
|
||||
<g *ngFor="let t of graphService.graphTabs">
|
||||
<g *ngFor="let l of t.column.displayLinks">
|
||||
<!-- <line class="svg-line-click" (click)="clickedLink(l)" [attr.x1]="l.x3" [attr.x2]="l.x4" [attr.y1]="l.y2" [attr.y2]="l.y2" marker-end="url(#arrow)"></line>
|
||||
<line class="svg-line-click" (click)="clickedLink(l)" [attr.x1]="l.x1" [attr.x2]="l.x3" [attr.y1]="l.y1" [attr.y2]="l.y2"></line>-->
|
||||
<line class="svg-line-visible" [class.svg-line-active]="l.weight > 1" [attr.x1]="l.x3" [attr.x2]="l.x4" [attr.y1]="l.y2" [attr.y2]="l.y2" marker-end="url(#arrow)"></line>
|
||||
<line class="svg-line-visible" [class.svg-line-active]="l.weight > 1" [attr.x1]="l.x1" [attr.x2]="l.x3" [attr.y1]="l.y1" [attr.y2]="l.y2"></line>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<!-- <div class="table-wrapper" *ngIf="tableData">
|
||||
</div>
|
||||
<div id="d3" class="d3">
|
||||
</div>-->
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,791 @@
|
|||
import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
|
||||
import * as d3 from 'd3';
|
||||
import * as d3Sankey from 'd3-sankey';
|
||||
import { DAG, SNode, GraphService, CategoryList, FilterCriteria, GraphTab } from '../graph.service';
|
||||
import { FullDocNode } from '../standard-map';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { debounce } from 'rxjs/operators';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { TreeModel, TreeNode, ITreeState } from 'angular-tree-component';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
import convert from 'color-convert';
|
||||
|
||||
import selection_attrs from 'd3-selection-multi/src/selection/attrs';
|
||||
d3.selection.prototype.attrs = selection_attrs;
|
||||
|
||||
class TableData
|
||||
{
|
||||
constructor(
|
||||
public headers: string[],
|
||||
public rows: SNode[][]) {
|
||||
}
|
||||
}
|
||||
|
||||
function saturateColor(input, saturationZeroToOne){
|
||||
if (input == "unset")
|
||||
return input;
|
||||
|
||||
var out = '#' + convert.keyword.hex(input) + saturationZeroToOne ;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-d3-test',
|
||||
templateUrl: './d3-test.component.html',
|
||||
styleUrls: [ './d3-test.component.css' ],
|
||||
encapsulation: ViewEncapsulation.None // Allow D3 to read styles through shadow DOM
|
||||
})
|
||||
export class D3TestComponent implements OnInit, OnDestroy {
|
||||
public graphType: number = 0;
|
||||
public graphData: DAG;
|
||||
public graphCategories: CategoryList = [];
|
||||
public graphCriteria = new FilterCriteria();
|
||||
public tableData: TableData = null;
|
||||
|
||||
public complianceColors = ["white", "green", "yellow", "red", "black"];
|
||||
public svgbgElement: any;
|
||||
private updateSubject = new Rx.BehaviorSubject(0);
|
||||
private updateViewSubject = new Rx.BehaviorSubject(0);
|
||||
private searchSubject = new Rx.BehaviorSubject(null);
|
||||
private tabsChangedSubscription;
|
||||
|
||||
constructor(
|
||||
public graphService: GraphService,
|
||||
private sanitizer: DomSanitizer) {
|
||||
|
||||
this.updateSubject.pipe(debounce(() => Rx.timer(1))).subscribe({
|
||||
next: (v) => this.updateGraph()
|
||||
});
|
||||
this.updateViewSubject.pipe(debounce(() => Rx.timer(1))).subscribe({
|
||||
next: (v) => this.updateGraphView()
|
||||
});
|
||||
this.searchSubject.pipe(debounce(() => Rx.timer(100))).subscribe({
|
||||
next: (v) => {
|
||||
if (v)
|
||||
this.filterFn(v[0], v[1]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.graphService.getDocTypes()
|
||||
.subscribe(dt => {
|
||||
this.graphCategories = dt;
|
||||
|
||||
//// activate first 3
|
||||
//for (var i = 0; i< 3; ++i)
|
||||
// this.graphCategories[i].active = true;
|
||||
|
||||
//this.RefreshGraph();
|
||||
});
|
||||
|
||||
this.tabsChangedSubscription = this.graphService.tabsChangedSubject.subscribe(a => {
|
||||
//this.tabChanged();
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.tabsChangedSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
public getMenuOptions(): any[] {
|
||||
var result = [];
|
||||
|
||||
if (this.graphService.canAdd)
|
||||
{
|
||||
for (var t of this.graphCategories)
|
||||
if (!this.graphService.graphTabs.find(g => g.title == t.id))
|
||||
result.push(t);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//public RefreshGraph() {
|
||||
//
|
||||
// var limitedDocs = this.graphCategories.filter(v => v.active).map(v => v.id);
|
||||
// if (limitedDocs.length > 0)
|
||||
// this.graphCriteria.categoryIds = limitedDocs;
|
||||
//
|
||||
// this.graphCriteria.categoryOrder = limitedDocs;
|
||||
//
|
||||
// if (this.graphType == 1)
|
||||
// {
|
||||
// // move ISO to second slot so it draws in the middle
|
||||
// var i0 = this.graphCriteria.categoryOrder[0];
|
||||
// var i1 = this.graphCriteria.categoryOrder[1];
|
||||
// this.graphCriteria.categoryOrder[0] = i1;
|
||||
// this.graphCriteria.categoryOrder[1] = i0;
|
||||
// }
|
||||
//
|
||||
// this.graphService.getGraphData2(this.graphCriteria)
|
||||
// .subscribe(gd => {
|
||||
// console.log(JSON.stringify(gd));
|
||||
// this.graphData = gd;
|
||||
//
|
||||
// switch (this.graphType)
|
||||
// {
|
||||
// case 0:
|
||||
// this.DrawTable(this.graphData);
|
||||
// break;
|
||||
// case 1:
|
||||
// this.DrawChart(this.graphData);
|
||||
// break;
|
||||
// case 2:
|
||||
// this.DrawGraph(this.graphData);
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
|
||||
private DrawTable(data: DAG) {
|
||||
var rowType = this.graphCriteria.categoryOrder ? this.graphCriteria.categoryOrder[0] : (data.nodes[0] as SNode).data.type;
|
||||
var headerTypes = this.graphCriteria.categoryOrder.filter(v => v != rowType);
|
||||
|
||||
var headerNodes = [rowType].concat(headerTypes);
|
||||
var rowNodes = data.nodes.filter(v => v.data.type == rowType).map(d => {
|
||||
var links = headerTypes.map(h => {
|
||||
for (var l of data.links)
|
||||
{
|
||||
var node = null;
|
||||
|
||||
if (l.source == d.nodeId && l.targetNode.data.type == h)
|
||||
node = l.targetNode;
|
||||
|
||||
if (l.target == d.nodeId && l.sourceNode.data.type == h)
|
||||
node = l.sourceNode;
|
||||
|
||||
if (node)
|
||||
return node;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
return [d].concat(links);
|
||||
});
|
||||
|
||||
var width = 960;
|
||||
var height = 500;
|
||||
|
||||
// clear
|
||||
d3.selectAll("#d3").selectAll("*").remove();
|
||||
|
||||
this.tableData = new TableData(headerNodes, rowNodes);
|
||||
}
|
||||
|
||||
private DrawChart(energy: DAG) {
|
||||
var width = 960;
|
||||
var height = 500;
|
||||
|
||||
// clear
|
||||
d3.selectAll("#d3").selectAll("*").remove();
|
||||
this.tableData = null;
|
||||
|
||||
var svg = d3.selectAll("#d3").append("svg");
|
||||
svg.attr("width", width);
|
||||
svg.attr("height", height);
|
||||
|
||||
var formatNumber = d3.format(",.0f"),
|
||||
format = function (d: any) { return formatNumber(d) + " TWh"; },
|
||||
color = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
|
||||
var sankey = d3Sankey.sankey()
|
||||
.nodeWidth(15)
|
||||
.nodePadding(10)
|
||||
.extent([[1, 1], [width - 1, height - 6]]);
|
||||
|
||||
var link = svg.append("g")
|
||||
.attr("class", "links")
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "#000")
|
||||
.attr("stroke-opacity", 0.2)
|
||||
.selectAll("path");
|
||||
|
||||
var node = svg.append("g")
|
||||
.attr("class", "nodes")
|
||||
.attr("font-family", "sans-serif")
|
||||
.attr("font-size", 10)
|
||||
.selectAll("g");
|
||||
|
||||
sankey.nodeAlign(d3Sankey.sankeyLeft);
|
||||
//sankey.nodeAlign((n, d) => {
|
||||
// return this.graphCriteria.categoryOrder.indexOf(n.data.type);
|
||||
//});
|
||||
sankey.nodeSort((a: SNode, b: SNode) => a.name < b.name ? -1 : 1);
|
||||
sankey.nodeId((d: SNode) => d.nodeId);
|
||||
sankey.nodeWidth(250);
|
||||
|
||||
sankey(energy);
|
||||
|
||||
link = link
|
||||
.data(energy.links);
|
||||
|
||||
link.enter().append("path")
|
||||
.attr("d", d3Sankey.sankeyLinkHorizontal())
|
||||
.attr("stroke-width", function (d: any) { return 10; }) //Math.max(1, d.width) * 0.; })
|
||||
.attr("stroke", d => "black") //color(d.source.name))
|
||||
.on('click', function(d, i) {
|
||||
console.log("clicked link", d);
|
||||
})
|
||||
.on("mouseover", function(d) {
|
||||
d3.select(this).style("cursor", "pointer");
|
||||
});
|
||||
|
||||
link.append("title")
|
||||
.text(function (d: any) { return d.source.name + " → " + d.target.name + "\n" + format(d.value) + "\n" + d.uom; });
|
||||
|
||||
node = node
|
||||
.data(energy.nodes)
|
||||
.enter().append("g");
|
||||
|
||||
node.append("rect")
|
||||
.attr("x", function (d: any) { return d.x0; })
|
||||
.attr("y", function (d: any) { return d.y0; })
|
||||
.attr("height", function (d: any) { return d.y1 - d.y0; })
|
||||
.attr("width", function (d: any) { return d.x1 - d.x0; })
|
||||
.attr("fill", d => { return this.complianceColors[energy.nodes[d.index].data.compliance_level]; } ) //color(d.name.replace(/ .*/, "")); })
|
||||
.attr("stroke", "#000")
|
||||
.on('click', function(d, i) {
|
||||
console.log("clicked node", d);
|
||||
})
|
||||
.on("mouseover", function(d) {
|
||||
d3.select(this).style("cursor", "pointer");
|
||||
});
|
||||
|
||||
node.append("text")
|
||||
.attr("x", function (d: any) { return d.x0 + 6; }) //{ return d.x0 - 6; })
|
||||
.attr("y", function (d: any) { return (d.y1 + d.y0) / 2; })
|
||||
.attr("dy", "0.35em")
|
||||
.attr("text-anchor", "start")
|
||||
.text(function (d: any) { return d.name + "\n" + d.data.body; });
|
||||
//.filter(function (d: any) { return d.x0 < width / 2; })
|
||||
//.attr("x", function (d: any) { return d.x1 + 6; })
|
||||
//.attr("text-anchor", "start");
|
||||
|
||||
node.append("title")
|
||||
.text(function (d: any) { return d.name + "\n" + d.data.body; });
|
||||
}
|
||||
|
||||
private DrawGraph(data: DAG) {
|
||||
var width = 960;
|
||||
var height = 500;
|
||||
|
||||
// clear
|
||||
d3.selectAll("#d3").selectAll("*").remove();
|
||||
this.tableData = null;
|
||||
|
||||
var svg = d3.selectAll("#d3").append("svg");
|
||||
svg.attr("width", width);
|
||||
svg.attr("height", height);
|
||||
|
||||
var link, node, edgelabels, edgepaths;
|
||||
|
||||
var colors = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
svg.append('defs').append('marker')
|
||||
.attrs({'id':'arrowhead',
|
||||
'viewBox':'-0 -5 10 10',
|
||||
'refX':13,
|
||||
'refY':0,
|
||||
'orient':'auto',
|
||||
'markerWidth':13,
|
||||
'markerHeight':13,
|
||||
'xoverflow':'visible'})
|
||||
.append('svg:path')
|
||||
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
|
||||
.attr('fill', '#999')
|
||||
.style('stroke','none');
|
||||
|
||||
var simulation = d3.forceSimulation()
|
||||
.force("link", d3.forceLink().id(function (d) {return d.nodeId;}).distance(100).strength(1))
|
||||
.force("charge", d3.forceManyBody())
|
||||
.force("center", d3.forceCenter(width / 2, height / 2));
|
||||
|
||||
update(data.links, data.nodes);
|
||||
|
||||
function update(links, nodes) {
|
||||
link = svg.selectAll(".link")
|
||||
.data(links)
|
||||
.enter()
|
||||
.append("line")
|
||||
.attr("class", "link")
|
||||
.attr('marker-end','url(#arrowhead)');
|
||||
|
||||
link.append("title")
|
||||
.text(function (d) {return ""; }); //d.type;});
|
||||
|
||||
edgepaths = svg.selectAll(".edgepath")
|
||||
.data(links)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attrs({
|
||||
'class': 'edgepath',
|
||||
'fill-opacity': 0,
|
||||
'stroke-opacity': 0,
|
||||
'id': function (d, i) {return 'edgepath' + i}
|
||||
})
|
||||
.style("pointer-events", "none");
|
||||
|
||||
edgelabels = svg.selectAll(".edgelabel")
|
||||
.data(links)
|
||||
.enter()
|
||||
.append('text')
|
||||
.style("pointer-events", "none")
|
||||
.attrs({
|
||||
'class': 'edgelabel',
|
||||
'id': function (d, i) {return 'edgelabel' + i},
|
||||
'font-size': 10,
|
||||
'fill': '#aaa'
|
||||
});
|
||||
|
||||
edgelabels.append('textPath')
|
||||
.attr('xlink:href', function (d, i) {return '#edgepath' + i})
|
||||
.style("text-anchor", "middle")
|
||||
.style("pointer-events", "none")
|
||||
.attr("startOffset", "50%")
|
||||
.text(function (d) {return ""; });
|
||||
|
||||
node = svg.selectAll(".node")
|
||||
.data(nodes)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "node")
|
||||
.call(d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
//.on("end", dragended)
|
||||
);
|
||||
|
||||
node.append("circle")
|
||||
.attr("r", 5)
|
||||
.style("fill", function (d, i) {return colors(i);})
|
||||
|
||||
node.append("title")
|
||||
.text(function (d) {return d.nodeId;});
|
||||
|
||||
node.append("text")
|
||||
.attr("dy", -3)
|
||||
.text(function (d) {return d.name;});
|
||||
|
||||
simulation
|
||||
.nodes(nodes)
|
||||
.on("tick", ticked);
|
||||
|
||||
simulation.force("link")
|
||||
.links(links);
|
||||
}
|
||||
|
||||
function ticked() {
|
||||
link
|
||||
.attr("x1", function (d) {return d.source.x;})
|
||||
.attr("y1", function (d) {return d.source.y;})
|
||||
.attr("x2", function (d) {return d.target.x;})
|
||||
.attr("y2", function (d) {return d.target.y;});
|
||||
|
||||
node
|
||||
.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
|
||||
|
||||
edgepaths.attr('d', function (d) {
|
||||
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
|
||||
});
|
||||
|
||||
edgelabels.attr('transform', function (d) {
|
||||
if (d.target.x < d.source.x) {
|
||||
var bbox = this.getBBox();
|
||||
|
||||
var rx = bbox.x + bbox.width / 2;
|
||||
var ry = bbox.y + bbox.height / 2;
|
||||
return 'rotate(180 ' + rx + ' ' + ry + ')';
|
||||
}
|
||||
else {
|
||||
return 'rotate(0)';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function dragstarted(d) {
|
||||
if (!d3.event.active) simulation.alphaTarget(0.3).restart()
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(d) {
|
||||
d.fx = d3.event.x;
|
||||
d.fy = d3.event.y;
|
||||
}
|
||||
}
|
||||
|
||||
public filter(value: string, treeModel: TreeModel) {
|
||||
this.searchSubject.next([value, treeModel]);
|
||||
}
|
||||
|
||||
public filterFn(value: string, treeModel: TreeModel) {
|
||||
if (value != "")
|
||||
treeModel.filterNodes((node: TreeNode) => this.fuzzysearch(value, node));
|
||||
else
|
||||
treeModel.filterNodes((node: TreeNode) => this.clearSearch(node));
|
||||
}
|
||||
|
||||
public tabChanged() {
|
||||
this.graphService.configureFilterStack();
|
||||
|
||||
if (this.graphService.selectedTab >= 0 && this.graphService.selectedTab < this.graphService.graphTabs.length)
|
||||
{
|
||||
this.graphService.graphTabs[this.graphService.selectedTab].parentTabTreeChanged(this.updateSubject);
|
||||
}
|
||||
}
|
||||
|
||||
public activateNode(tab: GraphTab, event: any) {
|
||||
if (tab.column.treeModel) {
|
||||
var newSelection = {};
|
||||
newSelection[tab.column.state.focusedNodeId] = true; // single select
|
||||
tab.column.state.activeNodeIds = newSelection;
|
||||
|
||||
this.updateSubject.next(0);
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(event) {
|
||||
//event.target.innerWidth;
|
||||
this.updateViewSubject.next(0);
|
||||
}
|
||||
|
||||
private buildLinkSet(fromTab: GraphTab, toTab: GraphTab, rtl: boolean): void {
|
||||
|
||||
// For all the links that are not filtered already
|
||||
var links = fromTab.visibleLinks;
|
||||
|
||||
// make a hash table from source _root_ node id, to list of links.
|
||||
// The source _root_ node is the first node in the ascenstery that is visible (parent is not collpased)
|
||||
var rollup = links.reduce((a, b) => {
|
||||
var owner = b.fromNode;
|
||||
|
||||
// Iterate to root, keep track of highest collapsed node.
|
||||
var iterator = owner;
|
||||
while (iterator.realParent)
|
||||
{
|
||||
iterator = iterator.realParent;
|
||||
if (iterator.isCollapsed)
|
||||
owner = iterator;
|
||||
}
|
||||
|
||||
// Create the list if it doesnt exist
|
||||
if (!(owner.id in a))
|
||||
a[owner.id] = [];
|
||||
|
||||
// add the link
|
||||
a[owner.id].push(b);
|
||||
return a;
|
||||
}, { });
|
||||
|
||||
// for each source root node, aggregate links to destination root nodes
|
||||
// destination _root_ nodes are the first node in the ascenstery that is visible (parent is not collpased)
|
||||
var rollup2 = Object.keys(rollup).map(k => {
|
||||
var collapsed = rollup[k].reduce((a, b) => {
|
||||
var link = b.link;
|
||||
var owner = toTab.treeModel.getNodeById(link.id);
|
||||
if (!owner) {
|
||||
toTab.parent.errors["- node not found: " + link.id + ", Referenced from: " + b.fromNode.id] = true;
|
||||
return a;
|
||||
}
|
||||
|
||||
// Iterate to root, keep track of highest collapsed node.
|
||||
var iterator = owner;
|
||||
while (iterator.realParent)
|
||||
{
|
||||
iterator = iterator.realParent;
|
||||
if (iterator.isCollapsed)
|
||||
owner = iterator;
|
||||
}
|
||||
|
||||
// Create the list if it doesnt exist
|
||||
if (!(owner.id in a))
|
||||
a[owner.id] = [];
|
||||
|
||||
// add the link
|
||||
a[owner.id].push(link);
|
||||
return a;
|
||||
}, { });
|
||||
|
||||
return [k, collapsed];
|
||||
});
|
||||
|
||||
// Create one aggregated list of links with all the necessary parameters for the view
|
||||
// start with the links map from source root node, to destination root nodes
|
||||
var flatten = rollup2.reduce((a, b) => {
|
||||
var fromTree = fromTab.treeModel;
|
||||
var toTree = toTab.treeModel;
|
||||
var destinationMap = b[1];
|
||||
var fromNode = fromTree.getNodeById(b[0]);
|
||||
|
||||
// If the source node is hidden, continue.
|
||||
if (fromNode.id in fromTree.hiddenNodeIds)
|
||||
return a;
|
||||
|
||||
// one source node may map to many destination.
|
||||
for (var destinationKey in destinationMap)
|
||||
{
|
||||
var toNode = toTree.getNodeById(destinationKey);
|
||||
if (!(toNode.id in toTree.hiddenNodeIds))
|
||||
{
|
||||
var destinationData = destinationMap[destinationKey];
|
||||
a.push({
|
||||
from: b[0],
|
||||
fromNode: fromNode,
|
||||
to: destinationKey,
|
||||
toNode: toNode,
|
||||
fromTree: fromTree,
|
||||
toTree: toTree,
|
||||
rtl: rtl,
|
||||
scale: rtl ? -1 : 1,
|
||||
weight: (fromNode.isActive || toNode.isActive) ? 2 : 1,
|
||||
x1: 0,
|
||||
x2: 0,
|
||||
x3: 0,
|
||||
x4: 0,
|
||||
y1: 0,
|
||||
y2: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}, []);
|
||||
|
||||
fromTab.displayLinks = flatten;
|
||||
}
|
||||
|
||||
public updateGraph() {
|
||||
var tabs = this.graphService.graphTabs;
|
||||
|
||||
// delay the rendering so dom can settle.
|
||||
//setTimeout(a => {
|
||||
var isoTab = tabs.find(t => t.isIso);
|
||||
var isoIndex = tabs.indexOf(isoTab);
|
||||
|
||||
for (var t = 0; t < tabs.length; ++t)
|
||||
{
|
||||
var tab = tabs[t];
|
||||
if (tab != isoTab)
|
||||
this.buildLinkSet(tab.column, isoTab.column, t > isoIndex);
|
||||
}
|
||||
|
||||
this.updateViewSubject.next(0);
|
||||
//}, 1);
|
||||
}
|
||||
|
||||
public updateGraphView() {
|
||||
if (!this.svgbgElement)
|
||||
return;
|
||||
|
||||
var tabs = this.graphService.graphTabs;
|
||||
var startingGapLeft = 0;
|
||||
var startingGapRight = 10;
|
||||
var arrowLength = 10;
|
||||
var svgBounds = this.svgbgElement.getBoundingClientRect();
|
||||
|
||||
for (var tab of tabs)
|
||||
{
|
||||
for (var l of tab.column.displayLinks)
|
||||
{
|
||||
var fromBounds = l.fromNode.elementRef2.getBoundingClientRect();
|
||||
var toBounds = l.toNode.elementRef2.getBoundingClientRect();
|
||||
|
||||
l.x1 = (l.rtl ? (fromBounds.left - startingGapRight) : (fromBounds.right + startingGapLeft)) - svgBounds.left;
|
||||
l.x2 = (l.rtl ? (toBounds.right + arrowLength) : (toBounds.left - arrowLength)) - svgBounds.left;
|
||||
l.y1 = fromBounds.top - svgBounds.top + fromBounds.height * 0.5;
|
||||
l.y2 = toBounds.top - svgBounds.top + toBounds.height * 0.5;
|
||||
|
||||
// Locations for the arrow head
|
||||
l.x3 = l.x2 - 2 * l.scale;
|
||||
l.x4 = l.x2 + 0.1 * l.scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setup(data, treeElement) {
|
||||
data.treeModel = treeElement.treeModel;
|
||||
treeElement.viewportComponent.elementRef.nativeElement.addEventListener('scroll', t => this.updateGraphView());
|
||||
}
|
||||
|
||||
public clickedLink(link: any) {
|
||||
link.fromTree.getNodeById(link.from).expandAll();
|
||||
link.toTree.getNodeById(link.to).expandAll();
|
||||
}
|
||||
|
||||
public bindTogether(node, element, svgbg) {
|
||||
node.elementRef2 = element;
|
||||
this.svgbgElement = svgbg;
|
||||
}
|
||||
|
||||
public clearSearch(node: TreeNode): boolean {
|
||||
node.data.highlight = undefined;
|
||||
return true;
|
||||
}
|
||||
|
||||
public fuzzysearch(searchTerm: string, node: TreeNode): boolean {
|
||||
var options = {
|
||||
includeMatches: true,
|
||||
includeScore: true,
|
||||
shouldSort: false,
|
||||
tokenize: false,
|
||||
threshold: 0.3,
|
||||
location: 0,
|
||||
distance: 800,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 3,
|
||||
};
|
||||
|
||||
var result = [];
|
||||
var scoreThresh = 0;
|
||||
var inName = false;
|
||||
|
||||
// Test body first
|
||||
if (node.data.node.body)
|
||||
{
|
||||
if (!node.data.bodyFuse)
|
||||
{
|
||||
node.data.bodyFuse = new Fuse([node.data.node.body], options);
|
||||
}
|
||||
|
||||
result = node.data.bodyFuse.search(searchTerm);
|
||||
result = result.filter(a => a.score > scoreThresh);
|
||||
}
|
||||
|
||||
if (result.length == 0)
|
||||
{
|
||||
// test title
|
||||
if (!node.data.sectionFuse)
|
||||
{
|
||||
node.data.sectionFuse = new Fuse([node.data.node.section], options);
|
||||
}
|
||||
|
||||
result = node.data.sectionFuse.search(searchTerm);
|
||||
result = result.filter(a => a.score > scoreThresh);
|
||||
inName = true;
|
||||
}
|
||||
|
||||
if (result.length > 0)
|
||||
{
|
||||
// record the longest matching span for highlight
|
||||
var length = -1;
|
||||
var longest = undefined;
|
||||
var match = result[0].matches[0];
|
||||
if (match)
|
||||
{
|
||||
for (var span of match.indices)
|
||||
{
|
||||
var newLength = span[1] - span[0];
|
||||
if (newLength > length)
|
||||
{
|
||||
length = newLength;
|
||||
longest = span;
|
||||
span[1] += 1; // convert ending index to js substring convention of end + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.data.highlight = longest;
|
||||
node.data.highlightName = inName;
|
||||
return true;
|
||||
}
|
||||
|
||||
node.data.highlight = undefined;
|
||||
return false;
|
||||
}
|
||||
|
||||
private highlightText(text: string, highlight: number[]): string
|
||||
{
|
||||
return text.substring(0, highlight[0]) + "<mark>" + text.substring(highlight[0], highlight[1]) + "</mark>" + text.substring(highlight[1], text.length);
|
||||
}
|
||||
|
||||
public injectHighlightSection(data: FullDocNode)
|
||||
{
|
||||
var section = data.node.section;
|
||||
|
||||
if (data.highlight && data.highlightName)
|
||||
section = this.highlightText(section, data.highlight);
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustHtml(section);
|
||||
}
|
||||
|
||||
public injectHighlightBody(data: FullDocNode)
|
||||
{
|
||||
var body = data.node.body;
|
||||
if (body)
|
||||
{
|
||||
if (data.highlight && !data.highlightName)
|
||||
{
|
||||
body = this.highlightText(body, data.highlight);
|
||||
}
|
||||
|
||||
body = " - " + body;
|
||||
}
|
||||
else
|
||||
body = "";
|
||||
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustHtml(body);
|
||||
}
|
||||
|
||||
public openTab(url: string)
|
||||
{
|
||||
window.open(url, "_blank").focus();
|
||||
}
|
||||
|
||||
public getNodeColor(tab: GraphTab, node: TreeNode)
|
||||
{
|
||||
// if we're a tree in the right side view, highlight active nodes
|
||||
var selected = (tab.parent && node.isActive);
|
||||
var color = selected ? 'lightblue' : 'unset';
|
||||
|
||||
if (tab.parent && node.data.filterColor)
|
||||
{
|
||||
if (!tab.isIso && node.data.isUnmapped)
|
||||
{
|
||||
color = 'red';
|
||||
}
|
||||
else if (!selected)
|
||||
{
|
||||
color = node.data.filterColor;
|
||||
}
|
||||
}
|
||||
else if (!tab.isIso)
|
||||
{
|
||||
// Iso never has outward mappings
|
||||
if (node.data.isUnmapped)
|
||||
{
|
||||
color = 'red';
|
||||
}
|
||||
else if (node.data.isAnyChildUnmapped)
|
||||
{
|
||||
color = 'pink';
|
||||
}
|
||||
}
|
||||
|
||||
// if we're a tree in the right side view
|
||||
if (tab.parent && !node.isActive)
|
||||
{
|
||||
// desaturate background color unless active node
|
||||
color = saturateColor(color, 'A0');
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
public filterMapped(tab: GraphTab)
|
||||
{
|
||||
tab.filterMapped();
|
||||
this.graphService.activateTab(tab);
|
||||
}
|
||||
|
||||
public filterIsoCoverage(tab: GraphTab)
|
||||
{
|
||||
var isoTab = this.graphService.graphTabs[1];
|
||||
isoTab.filterToIds(tab.coverage.uncoveredIds);
|
||||
this.graphService.activateTab(isoTab);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
/* DashboardComponent's private CSS styles */
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
<app-d3-test></app-d3-test>
|
||||
|
||||
<div *ngFor="let t of graphService.graphTabs">
|
||||
<div *ngIf="t.anyErrors">
|
||||
{{t.title}} Errors
|
||||
<div *ngFor="let e of t.errorStrings">
|
||||
<div>
|
||||
<h4>{{e}}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,58 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
import { StandardMapSearchComponent } from '../standard-map-search/standard-map-search.component';
|
||||
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { STANDARDMAPS } from '../mock-standard-map';
|
||||
import { StandardMapService } from '../standard-map.service';
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent;
|
||||
let fixture: ComponentFixture<DashboardComponent>;
|
||||
let standardMapService;
|
||||
let getStandardMapsSpy;
|
||||
|
||||
beforeEach(async(() => {
|
||||
standardMapService = jasmine.createSpyObj('StandardMapService', ['getStandardMaps']);
|
||||
getStandardMapsSpy = standardMapService.getStandardMaps.and.returnValue( of(STANDARDMAPS) );
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DashboardComponent,
|
||||
StandardMapSearchComponent
|
||||
],
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
providers: [
|
||||
{ provide: StandardMapService, useValue: standardMapService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display "Top Standard Maps" as headline', () => {
|
||||
expect(fixture.nativeElement.querySelector('h3').textContent).toEqual('Top Standard Maps');
|
||||
});
|
||||
|
||||
it('should call standardMapService', async(() => {
|
||||
expect(getStandardMapsSpy.calls.any()).toBe(true);
|
||||
}));
|
||||
|
||||
it('should display 4 links', async(() => {
|
||||
expect(fixture.nativeElement.querySelectorAll('a').length).toEqual(4);
|
||||
}));
|
||||
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { GraphService } from '../graph.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: [ './dashboard.component.css' ]
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public graphService: GraphService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,733 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
|
||||
import { Observable, of, forkJoin } from 'rxjs';
|
||||
import * as Rx from 'rxjs';
|
||||
import { catchError, map, tap, debounce } from 'rxjs/operators';
|
||||
|
||||
import { FullDocNode, DocNode2, Doc2, Link } from './standard-map';
|
||||
import { MessageService } from './message.service';
|
||||
import * as d3Sankey from 'd3-sankey';
|
||||
import { TreeModel, TreeNode, ITreeState, TREE_ACTIONS, IActionMapping } from 'angular-tree-component';
|
||||
|
||||
|
||||
export interface ICategory {
|
||||
id: string;
|
||||
title: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export type CategoryList = ICategory[];
|
||||
|
||||
export class FilterCriteria {
|
||||
constructor(
|
||||
public categoryIds: string[] = null,
|
||||
public categoryOrder: string[] = null) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -- Dag Node --
|
||||
export interface SNodeExtra {
|
||||
nodeId: number;
|
||||
name: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface SLinkExtra {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
uom: string;
|
||||
sourceNode: any;
|
||||
targetNode: any;
|
||||
}
|
||||
|
||||
export type SNode = d3Sankey.SankeyNode<SNodeExtra, SLinkExtra>;
|
||||
export type SLink = d3Sankey.SankeyLink<SNodeExtra, SLinkExtra>;
|
||||
|
||||
export interface DAG {
|
||||
nodes: SNode[];
|
||||
links: SLink[];
|
||||
}
|
||||
// -- Dag Node --
|
||||
|
||||
export class VisibleLink {
|
||||
constructor(
|
||||
public fromNode: TreeNode,
|
||||
public link: Link) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class GraphTab {
|
||||
public options = {
|
||||
useCheckbox: true,
|
||||
allowDrag: false,
|
||||
allowDrop: false,
|
||||
scrollOnActivate: false,
|
||||
scrollContainer: <HTMLElement>document.body, // Fix for bug: https://github.com/500tech/angular-tree-component/issues/704
|
||||
actionMapping: {
|
||||
mouse: {
|
||||
click: (tree, node, $event) => {
|
||||
TREE_ACTIONS.TOGGLE_ACTIVE_MULTI(tree, node, $event);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public state: ITreeState = { };
|
||||
public treeModel: TreeModel;
|
||||
public visibleNodes: TreeNode[] = [];
|
||||
public visibleLinks: VisibleLink[] = [];
|
||||
public displayLinks: any[] = [];
|
||||
public isIso: boolean = false;
|
||||
public searchValue: string = null;
|
||||
public coverage: any = null;
|
||||
public autoFilterSrc: GraphTab;
|
||||
public autoFilterSelf: boolean;
|
||||
public autoFilterParent: GraphTab;
|
||||
public errors: any = {};
|
||||
private updateSubjectParent = new Rx.BehaviorSubject(null);
|
||||
private updateSubjectColumn = new Rx.BehaviorSubject(null);
|
||||
|
||||
constructor(
|
||||
public title: string,
|
||||
public graphService: GraphService,
|
||||
public parent: GraphTab = null) {
|
||||
this.isIso = title == "ISO";
|
||||
|
||||
if (!parent)
|
||||
{
|
||||
this.column = new GraphTab(title, this.graphService, this);
|
||||
this.column.options.useCheckbox = false;
|
||||
|
||||
this.updateSubjectParent.pipe(debounce(() => Rx.timer(1))).subscribe({
|
||||
next: (v) => this.parentTabTreeChangedImp(v)
|
||||
});
|
||||
|
||||
this.updateSubjectColumn.pipe(debounce(() => Rx.timer(1))).subscribe({
|
||||
next: (v) => this.columnTabTreeChangedImp(v)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get errorStrings(): string[] {
|
||||
return Object.keys(this.errors);
|
||||
}
|
||||
|
||||
public get anyErrors(): boolean {
|
||||
return this.errorStrings.length > 0;
|
||||
}
|
||||
|
||||
public get anyExpanded():boolean {
|
||||
return this.treeModel && this.treeModel.expandedNodes.length > 0;
|
||||
}
|
||||
|
||||
public get anySelected():boolean {
|
||||
return this.treeModel && this.treeModel.selectedLeafNodeIds && Object.keys(this.treeModel.selectedLeafNodeIds).length > 0;
|
||||
}
|
||||
|
||||
public nodes = [];
|
||||
|
||||
public column: GraphTab;
|
||||
|
||||
public static filterBySelectedLeafs(visibleNodes: TreeNode[], parentTree: TreeModel, node: TreeNode, noSelection: boolean): boolean
|
||||
{
|
||||
var inSelection = node.data.id in parentTree.selectedLeafNodeIds;
|
||||
node.data.filterColor = inSelection ? "yellow" : undefined;
|
||||
|
||||
var show = noSelection || inSelection;
|
||||
if (show)
|
||||
visibleNodes.push(node);
|
||||
return show;
|
||||
}
|
||||
|
||||
public static containsNode(parentTree: VisibleLink[], node: TreeNode): boolean
|
||||
{
|
||||
return parentTree.find(n => {
|
||||
return n.link.id == node.data.id;
|
||||
}) != null
|
||||
}
|
||||
public static filterByVisibleLinks(visibleNodes: TreeNode[], parentTree: VisibleLink[], node: TreeNode): boolean
|
||||
{
|
||||
var show = GraphTab.containsNode(parentTree, node);
|
||||
|
||||
var parent = node.parent;
|
||||
while (!show && parent)
|
||||
{
|
||||
// didnt find us but see if any parent is linked
|
||||
show = GraphTab.containsNode(parentTree, parent);
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
if (show)
|
||||
visibleNodes.push(node);
|
||||
return show;
|
||||
}
|
||||
|
||||
public static filterByMyLinks(visibleNodes: TreeNode[], parentTree: GraphTab, node: TreeNode): boolean
|
||||
{
|
||||
// keep unmapped stuff by default
|
||||
var show = node.data.isUnmapped;
|
||||
|
||||
if (!show)
|
||||
{
|
||||
// include linked stuff
|
||||
var links = node.data.node.links;
|
||||
if (links)
|
||||
{
|
||||
for (var l of links)
|
||||
{
|
||||
if (!(l.id in parentTree.state.hiddenNodeIds) || !parentTree.state.hiddenNodeIds[l.id])
|
||||
{
|
||||
show = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (show)
|
||||
visibleNodes.push(node);
|
||||
return show;
|
||||
}
|
||||
|
||||
public runFilter()
|
||||
{
|
||||
this.visibleNodes = [];
|
||||
this.visibleLinks = [];
|
||||
|
||||
if (this.treeModel)
|
||||
{
|
||||
this.treeModel.clearFilter();
|
||||
|
||||
var noSelection = Object.keys(this.parent.treeModel.selectedLeafNodeIds).length == 0;
|
||||
|
||||
this.treeModel.filterNodes((node: TreeNode) => {
|
||||
var show = false;
|
||||
if (this.autoFilterSrc)
|
||||
{
|
||||
if (this.autoFilterSelf)
|
||||
{
|
||||
show = GraphTab.filterByMyLinks(this.visibleNodes, this.autoFilterSrc, node);
|
||||
}
|
||||
else
|
||||
show = GraphTab.filterByVisibleLinks(this.visibleNodes, this.autoFilterSrc.visibleLinks, node);
|
||||
}
|
||||
else
|
||||
{
|
||||
show = GraphTab.filterBySelectedLeafs(this.visibleNodes, this.parent.treeModel, node, noSelection);
|
||||
}
|
||||
|
||||
return show;
|
||||
}, false);
|
||||
|
||||
if (this.autoFilterParent)
|
||||
{
|
||||
if (this.autoFilterParent.anySelected)
|
||||
this.treeModel.expandAll();
|
||||
else
|
||||
this.treeModel.collapseAll();
|
||||
}
|
||||
|
||||
this.visibleLinks = GraphService.flatten(this.visibleNodes.map(v => {
|
||||
var links = v.data.node.links;
|
||||
return links ? links.map(l => new VisibleLink(v, l)) : [];
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public filterMapped()
|
||||
{
|
||||
// collapse all
|
||||
this.forAllTreeNodes(n => n.collapse());
|
||||
|
||||
this.treeModel.selectedLeafNodeIds = { };
|
||||
|
||||
var scrolledOnce = false;
|
||||
this.forAllTreeNodes(n => {
|
||||
if (n.level > 0)
|
||||
{
|
||||
n.setIsSelected(n.data.isUnmapped);
|
||||
if (n.data.isUnmapped)
|
||||
{
|
||||
n.ensureVisible();
|
||||
if (!scrolledOnce)
|
||||
{
|
||||
scrolledOnce = true;
|
||||
n.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public filterToIds(ids: string[])
|
||||
{
|
||||
// collapse all
|
||||
this.forAllTreeNodes(n => n.collapse());
|
||||
|
||||
this.treeModel.selectedLeafNodeIds = { };
|
||||
|
||||
var scrolledOnce = false;
|
||||
this.forAllTreeNodes(n => {
|
||||
if (n.level > 0)
|
||||
{
|
||||
var select = ids.includes(n.id);
|
||||
|
||||
n.setIsSelected(select);
|
||||
if (select)
|
||||
{
|
||||
n.ensureVisible();
|
||||
if (!scrolledOnce)
|
||||
{
|
||||
scrolledOnce = true;
|
||||
n.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
if (this.anyExpanded)
|
||||
{
|
||||
this.treeModel.collapseAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.treeModel.expandAll();
|
||||
}
|
||||
}
|
||||
|
||||
public selectAll() {
|
||||
if (this.anySelected)
|
||||
{
|
||||
this.treeModel.selectedLeafNodeIds = { };
|
||||
}
|
||||
else
|
||||
{
|
||||
this.treeModel.selectedLeafNodeIds = { };
|
||||
this.forAllTreeNodes(n => {
|
||||
if (n.level > 0)
|
||||
n.setIsSelected(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public mergedOptions(opts) {
|
||||
return Object.assign(this.options, opts);
|
||||
}
|
||||
|
||||
// Due to a bug in the tree control (forall is async but does not return the promise)
|
||||
// create our own synchronous forall
|
||||
public forAllTreeNodes(cb: (tn: TreeNode) => void)
|
||||
{
|
||||
for (var n of this.treeModel.roots)
|
||||
this.forAllTreeNodesRecursive(n, cb);
|
||||
}
|
||||
|
||||
private forAllTreeNodesRecursive(node: TreeNode, cb: (tn: TreeNode) => void)
|
||||
{
|
||||
cb(node);
|
||||
if (node.children)
|
||||
{
|
||||
for (var n of node.children)
|
||||
this.forAllTreeNodesRecursive(n, cb);
|
||||
}
|
||||
}
|
||||
|
||||
public parentTabTreeChanged(updateSubject: any) {
|
||||
this.updateSubjectParent.next([updateSubject]);
|
||||
}
|
||||
|
||||
public parentTabTreeChangedImp(data: any) {
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
var updateSubject = data[0];
|
||||
|
||||
if (this.column.treeModel) {
|
||||
|
||||
// Due to a bug https://github.com/500tech/angular-tree-component/issues/521
|
||||
// must manually clear nodes that are no longer selected
|
||||
for (var n of Object.keys(this.treeModel.selectedLeafNodeIds))
|
||||
{
|
||||
var node = this.treeModel.getNodeById(n);
|
||||
if (node && !node.isSelected)
|
||||
delete this.treeModel.selectedLeafNodeIds[n];
|
||||
}
|
||||
|
||||
this.graphService.runFilters(this, true, updateSubject);
|
||||
|
||||
updateSubject.next(0);
|
||||
|
||||
// Must be delayed or you'll get an infinite loop of change events.
|
||||
//setTimeout(() => {
|
||||
// by default, collapse everything
|
||||
this.column.forAllTreeNodes(n => n.collapse());
|
||||
|
||||
// ensure selected nodes are visible
|
||||
for (var n in this.treeModel.selectedLeafNodeIds)
|
||||
{
|
||||
var columnNode = this.column.treeModel.getNodeById(n);
|
||||
if (columnNode)
|
||||
columnNode.ensureVisible();
|
||||
}
|
||||
//}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public columnTabTreeChanged(event: any, updateSubject: any) {
|
||||
this.updateSubjectColumn.next([event, updateSubject]);
|
||||
}
|
||||
|
||||
public columnTabTreeChangedImp(data: any) {
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
var event = data[0];
|
||||
var updateSubject = data[1];
|
||||
if (this.column.treeModel) {
|
||||
if (event)
|
||||
{
|
||||
// on expand
|
||||
if (event.isExpanded && event.node.isActive)
|
||||
{
|
||||
var allNodes = this.graphService.getNodesWithLinks(event.node.data.children, []);
|
||||
|
||||
// select children with links
|
||||
for (var c of allNodes)
|
||||
{
|
||||
this.column.state.activeNodeIds[c.id] = true; // select child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.graphService.runFilters(this, false, updateSubject);
|
||||
|
||||
updateSubject.next(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class GraphService {
|
||||
private docGuids = {};
|
||||
private docDb = null;
|
||||
private nextDocGuid = 0;
|
||||
public tabsChangedSubject = new Rx.BehaviorSubject(0);
|
||||
|
||||
constructor(
|
||||
private messageService: MessageService,
|
||||
private http: HttpClient) {
|
||||
this.addTab("ISO");
|
||||
}
|
||||
|
||||
public static flatten(array) {
|
||||
return array.reduce((a,b)=>a.concat(b), []);
|
||||
}
|
||||
|
||||
public runFilters(changedTab: GraphTab, parentChanged: boolean, updateSubject: any)
|
||||
{
|
||||
var tabs = this.graphTabs;
|
||||
for (var t of tabs)
|
||||
{
|
||||
if (t.column.autoFilterSrc == changedTab.column || (parentChanged && t == changedTab))
|
||||
{
|
||||
// filter child tree
|
||||
t.column.runFilter();
|
||||
t.columnTabTreeChanged(null, updateSubject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getGuid(id: string, type: string, rev: string, createMissing: boolean = true): number {
|
||||
var key = `${type}-${id}-${rev}`;
|
||||
if (key in this.docGuids)
|
||||
return this.docGuids[key];
|
||||
|
||||
if (!createMissing)
|
||||
return null;
|
||||
|
||||
var value = this.nextDocGuid++;
|
||||
this.docGuids[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
getDb() : Observable<Doc2[]> {
|
||||
if (this.docDb)
|
||||
return of(this.docDb);
|
||||
|
||||
return this.http.get<Doc2[]>('assets/db.json', {responseType: 'json'})
|
||||
.pipe(
|
||||
tap(
|
||||
data => this.docDb = data,
|
||||
error => this.handleError("getDb", [])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getDocTypes() : Observable<CategoryList> {
|
||||
return this.getDb().pipe(
|
||||
map(
|
||||
data => {
|
||||
return data.map(v => { return { id: v.type, title: v.type }; });
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private addToDoc(parent: FullDocNode, input: DocNode2) {
|
||||
var child = new FullDocNode(input);
|
||||
|
||||
if (parent)
|
||||
{
|
||||
parent.children.push(child);
|
||||
}
|
||||
|
||||
// Recurse
|
||||
for (var c of input.children)
|
||||
{
|
||||
this.addToDoc(child, c);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
getFullDocByType(docType: string) : Observable<FullDocNode> {
|
||||
return this.getDb().pipe(
|
||||
map(
|
||||
data => {
|
||||
var doc = data.find(n => n.type == docType);
|
||||
return this.addToDoc(null, doc);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Live state management: maybe move this to a different service.
|
||||
public graphTabs: GraphTab[] = [ ];
|
||||
public selectedTab: number = 0;
|
||||
|
||||
public get canAdd(): boolean {
|
||||
return this.graphTabs.length < 3;
|
||||
}
|
||||
|
||||
public addTab(id: string) {
|
||||
if (!this.canAdd)
|
||||
return;
|
||||
|
||||
this.getFullDocByType(id)
|
||||
.subscribe(doc => {
|
||||
var newTab = new GraphTab(id, this);
|
||||
|
||||
newTab.nodes = doc.children;
|
||||
newTab.column.nodes = doc.children;
|
||||
|
||||
this.graphTabs.push(newTab);
|
||||
this.ensureISOIsInMiddle();
|
||||
|
||||
if (id != "ISO")
|
||||
{
|
||||
// compare with iso.
|
||||
newTab.coverage = this.compareDocs(newTab.column, this.graphTabs[1]);
|
||||
}
|
||||
|
||||
this.selectedTab = -1; // set it to non-value so change is detected if the index is the same
|
||||
setTimeout(() => this.activateTab(newTab), 1); // need to let dom regenerate
|
||||
|
||||
this.tabsChangedSubject.next(0);
|
||||
});
|
||||
}
|
||||
|
||||
private ensureISOIsInMiddle() {
|
||||
var isoTab = this.graphTabs.find(t => t.title == "ISO");
|
||||
|
||||
if (this.graphTabs.length > 1)
|
||||
{
|
||||
this.graphTabs = this.graphTabs.filter(t => t != isoTab);
|
||||
this.graphTabs.splice(1, 0, isoTab);
|
||||
}
|
||||
}
|
||||
|
||||
public configureFilterStack() {
|
||||
var filterOrder = [];
|
||||
switch (this.selectedTab)
|
||||
{
|
||||
case 0: filterOrder = [0, 1, 2]; break;
|
||||
case 1: filterOrder = [1, 0, 2]; break;
|
||||
case 2: filterOrder = [2, 1, 0]; break;
|
||||
}
|
||||
|
||||
// setup filters
|
||||
var isoTab = this.graphTabs.find(t => t.title == "ISO");
|
||||
var primary = this.graphTabs[filterOrder[0]];
|
||||
|
||||
if (!primary)
|
||||
return;
|
||||
|
||||
// clear auto filter of left tab
|
||||
primary.column.autoFilterSrc = null;
|
||||
primary.column.autoFilterParent = null;
|
||||
primary.column.autoFilterSelf = false;
|
||||
|
||||
var secondary = this.graphTabs[filterOrder[1]];
|
||||
if (secondary)
|
||||
{
|
||||
if (secondary == isoTab)
|
||||
{
|
||||
// assure iso filters from the primary: "auto filter"
|
||||
isoTab.column.autoFilterSrc = primary.column;
|
||||
isoTab.column.autoFilterParent = primary.column.parent;
|
||||
isoTab.column.autoFilterSelf = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// auto filter with this tabs connections to iso
|
||||
secondary.column.autoFilterSrc = isoTab.column;
|
||||
secondary.column.autoFilterParent = primary.column.parent; // the primary tab always drives the selection
|
||||
secondary.column.autoFilterSelf = true;
|
||||
}
|
||||
}
|
||||
|
||||
var third = this.graphTabs[filterOrder[2]];
|
||||
if (third)
|
||||
{
|
||||
// auto filter with this tabs connections to iso
|
||||
third.column.autoFilterSrc = isoTab.column;
|
||||
third.column.autoFilterParent = primary.column.parent; // the primary tab always drives the selection
|
||||
third.column.autoFilterSelf = true;
|
||||
}
|
||||
}
|
||||
|
||||
public removeTab(tab) {
|
||||
this.graphTabs = this.graphTabs.filter(t => t!=tab);
|
||||
this.ensureISOIsInMiddle();
|
||||
this.activateTab(this.graphTabs[0]);
|
||||
}
|
||||
|
||||
public activateTab(tab: GraphTab) {
|
||||
var newIndex = this.graphTabs.indexOf(tab);
|
||||
if (newIndex != this.selectedTab)
|
||||
{
|
||||
this.selectedTab = newIndex;
|
||||
this.configureFilterStack();
|
||||
this.tabsChangedSubject.next(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selectedTab = -1;
|
||||
setTimeout(() => {
|
||||
this.selectedTab = newIndex;
|
||||
this.configureFilterStack();
|
||||
this.tabsChangedSubject.next(0);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
public getNodesWithLinks(children: FullDocNode[], result: FullDocNode[])
|
||||
{
|
||||
for (var c of children)
|
||||
{
|
||||
if (c.node.links && c.node.links.length > 0)
|
||||
result.push(c);
|
||||
this.getNodesWithLinks(c.children, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public flattenSections(children: FullDocNode[], result: string[])
|
||||
{
|
||||
for (var c of children)
|
||||
{
|
||||
if (c.node.body)
|
||||
result.push(c.id);
|
||||
this.flattenSections(c.children, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public flattenLinks(children: FullDocNode[], result: Link[], linkData: any)
|
||||
{
|
||||
for (var c of children)
|
||||
{
|
||||
if (c.shouldBeMapped)
|
||||
{
|
||||
linkData.total++;
|
||||
if (!c.isUnmapped)
|
||||
{
|
||||
linkData.linked++;
|
||||
result = result.concat(c.node.links);
|
||||
}
|
||||
}
|
||||
result = this.flattenLinks(c.children, result, linkData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public compareDocs(aTab: GraphTab, bTab: GraphTab): any {
|
||||
var bSections = [];
|
||||
this.flattenSections(bTab.nodes, bSections);
|
||||
var bCopy = bSections.slice();
|
||||
|
||||
var linkData = { total: 0, linked: 0 };
|
||||
var aLinks = this.flattenLinks(aTab.nodes, [], linkData);
|
||||
|
||||
var found = 0;
|
||||
var checked = 0;
|
||||
for (var a of aLinks)
|
||||
{
|
||||
++checked;
|
||||
var b = bCopy.find(x => x == a.id)
|
||||
if (b)
|
||||
{
|
||||
bCopy = bCopy.filter(x => x != b);
|
||||
++found;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
coverage: found + "/" + bSections.length,
|
||||
mapped: linkData.linked + "/" + linkData.total,
|
||||
uniqueconnections: found + "/" + checked,
|
||||
uncoveredIds: bCopy
|
||||
|
||||
//"coverage": (found / bSections.length * 100).toFixed(1) + "% (" + found + "/" + bSections.length + ")",
|
||||
//"mapped": (linkData.linked / linkData.total * 100).toFixed(1) + "% (" + linkData.linked + "/" + linkData.total + ")",
|
||||
//"uniqueconnections": (found / checked * 100).toFixed(1) + "% (" + found + "/" + checked + ")"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Http operation that failed.
|
||||
* Let the app continue.
|
||||
* @param operation - name of the operation that failed
|
||||
* @param result - optional value to return as the observable result
|
||||
*/
|
||||
private handleError<T> (operation = 'operation', result?: T) {
|
||||
return (error: any): Observable<T> => {
|
||||
|
||||
// TODO: send the error to remote logging infrastructure
|
||||
console.error(error); // log to console instead
|
||||
|
||||
// TODO: better job of transforming error for user consumption
|
||||
this.log(`${operation} failed: ${error.message}`);
|
||||
|
||||
// Let the app keep running by returning an empty result.
|
||||
return of(result as T);
|
||||
};
|
||||
}
|
||||
|
||||
/** Log a GraphService message with the MessageService */
|
||||
private log(message: string) {
|
||||
this.messageService.add(`GraphService: ${message}`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { mapDb } from './mock-standard-maps'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class InMemoryDataService implements InMemoryDbService {
|
||||
createDb() {
|
||||
const standardMaps = mapDb.slice();
|
||||
return {'standard-map':standardMaps};
|
||||
}
|
||||
|
||||
//// Overrides the genId method to ensure that a standard-map always has an id.
|
||||
//// If the standardMaps array is empty,
|
||||
//// the method below returns the initial number (11).
|
||||
//// if the standardMaps array is not empty, the method below returns the highest
|
||||
//// standard-map id + 1.
|
||||
//genId(standardMaps: StandardMap[]): number {
|
||||
// return standardMaps.length > 0 ? Math.max(...standardMaps.map(standardMap => standardMap.id)) + 1 : 11;
|
||||
//}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { MessageService } from './message.service';
|
||||
|
||||
describe('MessageService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [MessageService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([MessageService], (service: MessageService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MessageService {
|
||||
messages: string[] = [];
|
||||
|
||||
add(message: string) {
|
||||
this.messages.push(message);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.messages = [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/* MessagesComponent's private CSS styles */
|
||||
h2 {
|
||||
color: red;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-weight: lighter;
|
||||
}
|
||||
body {
|
||||
margin: 2em;
|
||||
}
|
||||
body, input[text], button {
|
||||
color: crimson;
|
||||
font-family: Cambria, Georgia;
|
||||
}
|
||||
|
||||
button.clear {
|
||||
font-family: Arial;
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #eee;
|
||||
color: #aaa;
|
||||
cursor: auto;
|
||||
}
|
||||
button.clear {
|
||||
color: #888;
|
||||
margin-bottom: 12px;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<div *ngIf="messageService.messages.length">
|
||||
|
||||
<h2>Messages</h2>
|
||||
<button class="clear"
|
||||
(click)="messageService.clear()">clear</button>
|
||||
<div *ngFor='let message of messageService.messages'> {{message}} </div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MessagesComponent } from './messages.component';
|
||||
|
||||
describe('MessagesComponent', () => {
|
||||
let component: MessagesComponent;
|
||||
let fixture: ComponentFixture<MessagesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ MessagesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MessagesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MessageService } from '../message.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-messages',
|
||||
templateUrl: './messages.component.html',
|
||||
styleUrls: ['./messages.component.css']
|
||||
})
|
||||
export class MessagesComponent implements OnInit {
|
||||
|
||||
constructor(public messageService: MessageService) {}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { DocNode2, Doc2 } from './standard-map';
|
||||
//import * as mockMapDb from './data/msftgdprsample.json'
|
||||
import * as mockMapDb from './data/sampledb.json'
|
||||
|
||||
interface SampleModule {
|
||||
default: Doc2[]
|
||||
};
|
||||
|
||||
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
|
||||
var mockText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel velit magna. Ut ex est, consequat ultricies aliquam in, rutrum quis tortor. Etiam ex turpis, lacinia ac blandit eget, ultrices ac urna. Morbi pulvinar, leo vel gravida lacinia, velit dolor iaculis est, at porta arcu leo tempus lacus. Proin tincidunt, urna non varius mollis, turpis nisl hendrerit felis, nec scelerisque dui velit a tellus. Praesent consequat ante lacus, ac euismod libero elementum lacinia. Ut libero turpis, interdum id quam quis, malesuada dapibus tortor. Nulla ut orci eu odio pretium gravida et et nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Curabitur lorem magna, malesuada in placerat scelerisque, lacinia sit amet dui. Vestibulum non elementum dui. Proin et pellentesque nisl, efficitur congue tortor. Cras dapibus vel libero vel efficitur. Duis vitae diam quam. Vivamus ornare ullamcorper dolor, at facilisis est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.";
|
||||
|
||||
|
||||
function GenerateStandardMapRecursive(doc: Doc2, result: DocNode2, sections: number, depth: number): void {
|
||||
if (depth > 0)
|
||||
{
|
||||
for (var i = 1; i <= sections; ++i)
|
||||
{
|
||||
var body = null;
|
||||
if (depth == 1)
|
||||
{
|
||||
var textLen = getRandomInt(40);
|
||||
var textStart = getRandomInt(mockText.length - textLen);
|
||||
body = mockText.substr(textStart, textLen);
|
||||
}
|
||||
|
||||
var newId = i.toString();
|
||||
|
||||
// prepend parent
|
||||
if (result.id)
|
||||
newId = result.id + "." + newId;
|
||||
|
||||
var child = {
|
||||
id: newId,
|
||||
section: newId,
|
||||
body: body,
|
||||
compliance_level: 1,
|
||||
children: [],
|
||||
links: []
|
||||
};
|
||||
|
||||
if (body && doc.type != "ISO")
|
||||
{
|
||||
// Link to iso doc
|
||||
child.links.push({
|
||||
id: newId,
|
||||
type: "ISO",
|
||||
rev: "1"
|
||||
});
|
||||
}
|
||||
|
||||
result.children.push(child);
|
||||
|
||||
GenerateStandardMapRecursive(doc, child, sections / 2, depth - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function GenerateStandardMap(type: string, rev: string, sections: number, depth: number): Doc2 {
|
||||
var result = {
|
||||
type: type,
|
||||
rev: rev,
|
||||
children: [],
|
||||
links: []
|
||||
};
|
||||
|
||||
GenerateStandardMapRecursive(result, result, sections, depth);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export var mapDb: Doc2[] = (mockMapDb as any as SampleModule).default;
|
||||
|
||||
//export var mapDb2: Doc2[] = [
|
||||
// GenerateStandardMap('ISO', "1", 8, 3),
|
||||
// GenerateStandardMap('PIPEDA', "1", 8, 3),
|
||||
// GenerateStandardMap('APP', "1", 8, 3)
|
||||
//];
|
|
@ -0,0 +1,57 @@
|
|||
/* StandardMapDetailComponent's private CSS styles */
|
||||
|
||||
.container {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 3em;
|
||||
margin: .5em 0;
|
||||
color: #607D8B;
|
||||
font-weight: bold;
|
||||
}
|
||||
input {
|
||||
height: 2em;
|
||||
font-size: 1em;
|
||||
padding-left: .4em;
|
||||
}
|
||||
button {
|
||||
margin-top: 20px;
|
||||
font-family: Arial;
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer; cursor: hand;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #eee;
|
||||
color: #ccc;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.link-field {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
height: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.example-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<div *ngIf="standardMap" class="container">
|
||||
<button (click)="goBack()">Go back</button>
|
||||
<h2>{{standardMap.name | uppercase}}</h2>
|
||||
|
||||
<div><mat-checkbox (change)="treeData.expandAll()" [checked]="treeData.anyExpanded">Expand All</mat-checkbox></div>
|
||||
|
||||
<tree-root #tree [focused]="true" [(state)]="treeData.state" [options]="treeData.options" [nodes]="treeData.nodes" (initialized)="treeData.treeModel = tree.treeModel;" >
|
||||
<ng-template #treeNodeWrapperTemplate let-node let-index="index">
|
||||
<div #wrapper class="node-wrapper" [style.padding-left]="node.getNodePadding()">
|
||||
<tree-node-expander [node]="node"></tree-node-expander>
|
||||
<div class="node-content-wrapper"
|
||||
[class.node-content-wrapper-focused]="node.isFocused"
|
||||
(click)="node.mouseAction('click', $event)"
|
||||
(dblclick)="node.mouseAction('dblClick', $event)"
|
||||
(contextmenu)="node.mouseAction('contextMenu', $event)"
|
||||
(treeDrop)="node.onDrop($event)"
|
||||
[treeAllowDrop]="node.allowDrop"
|
||||
[treeDrag]="node"
|
||||
[treeDragEnabled]="node.allowDrag()">
|
||||
|
||||
<div class="node-content">
|
||||
<div *ngIf="(node.isExpanded || node.isLeaf)">
|
||||
<div class="button-row">
|
||||
<span class="example-spacer"></span>
|
||||
<button><mat-icon>arrow_upward</mat-icon></button>
|
||||
<button><mat-icon>arrow_downward</mat-icon></button>
|
||||
<button><mat-icon>cancel</mat-icon></button>
|
||||
</div>
|
||||
<mat-form-field class="input-field">
|
||||
<input matInput type="text" placeholder="id" [(ngModel)]="node.data.node.id" >
|
||||
</mat-form-field>
|
||||
<mat-form-field class="input-field">
|
||||
<input matInput type="text" placeholder="section" [(ngModel)]="node.data.node.section" >
|
||||
</mat-form-field>
|
||||
<mat-form-field class="input-field">
|
||||
<input matInput type="text" placeholder="body" [(ngModel)]="node.data.node.body" >
|
||||
</mat-form-field>
|
||||
<!--<tree-node-content [node]="node" [index]="index"></tree-node-content>-->
|
||||
<h4>
|
||||
<span>Links</span>
|
||||
<span class="example-spacer"></span>
|
||||
<button><mat-icon>add_box</mat-icon></button>
|
||||
</h4>
|
||||
<div *ngFor="let link of node.data.node.links">
|
||||
<div class="link">
|
||||
<mat-form-field class="link-field">
|
||||
<input matInput type="text" placeholder="type" [(ngModel)]="link.type" >
|
||||
</mat-form-field>
|
||||
<mat-form-field class="link-field">
|
||||
<input matInput type="text" placeholder="id" [(ngModel)]="link.id" >
|
||||
</mat-form-field>
|
||||
<mat-form-field class="link-field">
|
||||
<input matInput type="text" placeholder="rev" [(ngModel)]="link.rev" >
|
||||
</mat-form-field>
|
||||
<button><mat-icon>cancel</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
<h4 *ngIf="node.children.length">Children</h4>
|
||||
</div>
|
||||
<div *ngIf="!(node.isExpanded || node.isLeaf)">
|
||||
<div>{{node.data.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</tree-root>
|
||||
</div>
|
|
@ -0,0 +1,47 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
import { FullDocNode } from '../standard-map';
|
||||
import { GraphService, GraphTab } from '../graph.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-standard-map-detail',
|
||||
templateUrl: './standard-map-detail.component.html',
|
||||
styleUrls: [ './standard-map-detail.component.css' ]
|
||||
})
|
||||
export class StandardMapDetailComponent implements OnInit {
|
||||
@Input() standardMap: FullDocNode;
|
||||
public treeData: GraphTab;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private graphService: GraphService,
|
||||
private location: Location
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.treeData == null)
|
||||
{
|
||||
// if it's not already injected, then get it from the url query
|
||||
this.treeData = new GraphTab("test", this.graphService);
|
||||
|
||||
this.route.params.subscribe(params => {
|
||||
this.graphService.getFullDocByType(params['id'])
|
||||
.subscribe(standardMap => {
|
||||
this.standardMap = standardMap;
|
||||
this.treeData.nodes = standardMap.children;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
//save(): void {
|
||||
// this.standardMapService.updateStandardMap(this.standardMap)
|
||||
// .subscribe(() => this.goBack());
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/* standardMapSearch private styles */
|
||||
.search-result li {
|
||||
border-bottom: 1px solid gray;
|
||||
border-left: 1px solid gray;
|
||||
border-right: 1px solid gray;
|
||||
width: 195px;
|
||||
height: 16px;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.search-result li:hover {
|
||||
background-color: #607D8B;
|
||||
}
|
||||
|
||||
.search-result li a {
|
||||
color: #888;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.search-result li a:hover {
|
||||
color: white;
|
||||
}
|
||||
.search-result li a:active {
|
||||
color: white;
|
||||
}
|
||||
#search-box {
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
|
||||
ul.search-result {
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<div id="search-component">
|
||||
<span>Search:</span><input #searchBox id="search-box" (input)="search(searchBox.value)" />
|
||||
|
||||
<ul class="search-result">
|
||||
<li *ngFor="let standardMap of standardMap$ | async" >
|
||||
<a routerLink="/detail/{{standardMap.id}}">
|
||||
{{standardMap.name}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,29 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
|
||||
import { StandardMapSearchComponent } from './standard-map-search.component';
|
||||
|
||||
|
||||
describe('StandardMapSearchComponent', () => {
|
||||
let component: StandardMapSearchComponent;
|
||||
let fixture: ComponentFixture<StandardMapSearchComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StandardMapSearchComponent ],
|
||||
imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StandardMapSearchComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import {
|
||||
debounceTime, distinctUntilChanged, switchMap
|
||||
} from 'rxjs/operators';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-standard-map-search',
|
||||
templateUrl: './standard-map-search.component.html',
|
||||
styleUrls: [ './standard-map-search.component.css' ]
|
||||
})
|
||||
export class StandardMapSearchComponent implements OnInit {
|
||||
standardMap$: Observable<any[]>;
|
||||
private searchTerms = new Subject<string>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
// Push a search term into the observable stream.
|
||||
search(term: string): void {
|
||||
this.searchTerms.next(term);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
//this.standardMap$ = this.searchTerms.pipe(
|
||||
// // wait 300ms after each keystroke before considering the term
|
||||
// debounceTime(300),
|
||||
//
|
||||
// // ignore new term if same as previous term
|
||||
// distinctUntilChanged(),
|
||||
//
|
||||
// // switch to new search observable each time the term changes
|
||||
// //switchMap((term: string) => this.standardMapService.searchStandardMaps(term)),
|
||||
//);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
export class Link {
|
||||
id: string;
|
||||
type: string;
|
||||
rev: string;
|
||||
}
|
||||
|
||||
export class DocNode2 {
|
||||
id?: string;
|
||||
section?: string;
|
||||
body?: string;
|
||||
hyperlink?: string;
|
||||
compliance_level?: number;
|
||||
children: DocNode2[];
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
|
||||
export class Doc2 extends DocNode2 {
|
||||
type: string;
|
||||
rev?: string;
|
||||
}
|
||||
|
||||
export class FullDocNode {
|
||||
public highlight: number[];
|
||||
public highlightName: boolean;
|
||||
public bodyFuse: any;
|
||||
public sectionFuse: any;
|
||||
public isUnmappedCached: boolean;
|
||||
public isAnyChildUnmappedCached: boolean;
|
||||
public shouldBeMappedCached: boolean;
|
||||
public filterColor: string;
|
||||
|
||||
public constructor(
|
||||
public node: Doc2 | DocNode2,
|
||||
public children: FullDocNode[] = []) {
|
||||
}
|
||||
|
||||
get name():string {
|
||||
var name = this.node.section ? this.node.section : (this.node as Doc2).type;
|
||||
if (this.node.body)
|
||||
name += " - " + this.node.body;
|
||||
return name;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.node.id;
|
||||
}
|
||||
|
||||
get isAnyChildUnmapped(): boolean {
|
||||
if (this.isAnyChildUnmappedCached === undefined)
|
||||
{
|
||||
this.isAnyChildUnmappedCached = false;
|
||||
for (var c of this.children)
|
||||
{
|
||||
if (c.isUnmapped || c.isAnyChildUnmapped)
|
||||
{
|
||||
this.isAnyChildUnmappedCached = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.isAnyChildUnmappedCached;
|
||||
}
|
||||
|
||||
get isUnmapped(): boolean {
|
||||
if (this.isUnmappedCached === undefined)
|
||||
{
|
||||
// to be qualified as unmapped
|
||||
this.isUnmappedCached = this.shouldBeMapped
|
||||
&& (!this.node.links || this.node.links.length == 0); // no links
|
||||
}
|
||||
|
||||
return this.isUnmappedCached;
|
||||
}
|
||||
|
||||
get shouldBeMapped(): boolean {
|
||||
if (this.shouldBeMappedCached === undefined)
|
||||
{
|
||||
// to be qualified as unmapped
|
||||
this.shouldBeMappedCached = this.node.body // needs a body
|
||||
&& (!this.node.children || this.node.children.length == 0); // no children
|
||||
}
|
||||
|
||||
return this.shouldBeMappedCached;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/* StandardMapsComponent's private CSS styles */
|
||||
.standard-maps {
|
||||
margin: 0 0 2em 0;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 15em;
|
||||
}
|
||||
.standard-maps li {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: .3em 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.standard-maps li:hover {
|
||||
color: #607D8B;
|
||||
background-color: #DDD;
|
||||
left: .1em;
|
||||
}
|
||||
|
||||
.standard-maps a {
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.standard-maps a:hover {
|
||||
color:#607D8B;
|
||||
}
|
||||
|
||||
.standard-maps .badge {
|
||||
display: inline-block;
|
||||
font-size: small;
|
||||
color: white;
|
||||
padding: 0.8em 0.7em 0 0.7em;
|
||||
background-color: #607D8B;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
top: -4px;
|
||||
height: 1.8em;
|
||||
min-width: 16px;
|
||||
text-align: right;
|
||||
margin-right: .8em;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
}
|
||||
|
||||
button.delete {
|
||||
position: relative;
|
||||
left: 194px;
|
||||
top: -32px;
|
||||
background-color: gray !important;
|
||||
color: white;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<h2>My StandardMaps</h2>
|
||||
|
||||
<!--<div>
|
||||
<label>StandardMap name:
|
||||
<input #standardMapName />
|
||||
</label>
|
||||
<button (click)="add(standardMapName.value); standardMapName.value=''">
|
||||
add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="standard-maps">
|
||||
<li *ngFor="let standardMap of standardMaps">
|
||||
<a routerLink="/detail/{{standardMap.id}}">
|
||||
<span class="badge">{{standardMap.id}}</span> {{standardMap.name}}
|
||||
</a>
|
||||
<button class="delete" title="delete standardMap"
|
||||
(click)="delete(standardMap)">x</button>
|
||||
</li>
|
||||
</ul>-->
|
|
@ -0,0 +1,27 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { StandardMapsComponent } from './standard-maps.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
|
||||
describe('StandardMapsComponent', () => {
|
||||
let component: StandardMapsComponent;
|
||||
let fixture: ComponentFixture<StandardMapsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StandardMapsComponent ],
|
||||
imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StandardMapsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
//import { StandardMap } from '../standard-map';
|
||||
|
||||
@Component({
|
||||
selector: 'app-standard-maps',
|
||||
templateUrl: './standard-maps.component.html',
|
||||
styleUrls: ['./standard-maps.component.css']
|
||||
})
|
||||
export class StandardMapsComponent implements OnInit {
|
||||
//standardMaps: StandardMap[];
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
//this.getStandardMaps();
|
||||
}
|
||||
|
||||
//getStandardMaps(): void {
|
||||
//}
|
||||
//
|
||||
//add(name: string): void {
|
||||
// //name = name.trim();
|
||||
// //if (!name) { return; }
|
||||
// //this.standardMapService.addStandardMap({ name } as StandardMap)
|
||||
// // .subscribe(standardMap => {
|
||||
// // this.standardMaps.push(standardMap);
|
||||
// // });
|
||||
//}
|
||||
//
|
||||
//delete(standardMap: StandardMap): void {
|
||||
//}
|
||||
|
||||
}
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>github-circle-white-transparent</title><path d="M10 0C4.477 0 0 4.477 0 10c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V19c0 .27.16.59.67.5C17.14 18.16 20 14.42 20 10A10 10 0 0 0 10 0z" fill="#FFF" fill-rule="evenodd"/></svg>
|
|
@ -0,0 +1,11 @@
|
|||
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
#
|
||||
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11
|
|
@ -0,0 +1,3 @@
|
|||
export const environment = {
|
||||
production: true
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 12 KiB |
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Standard Maps Explorer</title>
|
||||
<base href="/">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,31 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, '../coverage'),
|
||||
reports: ['html', 'lcovonly'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||
// import 'core-js/es6/symbol';
|
||||
// import 'core-js/es6/object';
|
||||
// import 'core-js/es6/function';
|
||||
// import 'core-js/es6/parse-int';
|
||||
// import 'core-js/es6/parse-float';
|
||||
// import 'core-js/es6/number';
|
||||
// import 'core-js/es6/math';
|
||||
// import 'core-js/es6/string';
|
||||
// import 'core-js/es6/date';
|
||||
// import 'core-js/es6/array';
|
||||
// import 'core-js/es6/regexp';
|
||||
// import 'core-js/es6/map';
|
||||
// import 'core-js/es6/weak-map';
|
||||
// import 'core-js/es6/set';
|
||||
|
||||
/**
|
||||
* If the application will be indexed by Google Search, the following is required.
|
||||
* Googlebot uses a renderer based on Chrome 41.
|
||||
* https://developers.google.com/search/docs/guides/rendering
|
||||
**/
|
||||
// import 'core-js/es6/array';
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following for the Reflect API. */
|
||||
/**
|
||||
* DO NOT REMOVE
|
||||
* By default, Reflect polyfills are auto-included by the CLI and
|
||||
* are required for JIT compilation. StackBlitz examples are
|
||||
* compiled using JIT.
|
||||
*/
|
||||
import 'core-js/es6/reflect';
|
||||
import 'core-js/es7/reflect';
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
**/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
*/
|
||||
|
||||
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
|
||||
/*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*/
|
||||
// (window as any).__Zone_enable_cross_context_check = true;
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
|
@ -0,0 +1,15 @@
|
|||
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.basic-container {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.mat-drawer-content {
|
||||
padding: 20px;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: any;
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.avoid.ts",
|
||||
"**/*.0.ts",
|
||||
"**/*.1.ts",
|
||||
"**/*.1b.ts",
|
||||
"**/*.2.ts",
|
||||
"**/*.3.ts",
|
||||
"**/*.4.ts",
|
||||
"**/*.5.ts",
|
||||
"**/*.6.ts",
|
||||
"**/*.7.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"test.ts",
|
||||
"polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../tslint.json",
|
||||
"rules": {
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
{
|
||||
"rulesDirectory": [
|
||||
"node_modules/codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"arrow-return-shorthand": true,
|
||||
"callable-types": true,
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"curly": true,
|
||||
"deprecation": {
|
||||
"severity": "warn"
|
||||
},
|
||||
"eofline": true,
|
||||
"forin": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"interface-over-type-literal": true,
|
||||
"label-position": true,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-arg": true,
|
||||
"no-bitwise": true,
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-super": true,
|
||||
"no-empty": false,
|
||||
"no-empty-interface": true,
|
||||
"no-eval": true,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-misused-new": true,
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-string-literal": false,
|
||||
"no-string-throw": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unnecessary-initializer": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-var-keyword": true,
|
||||
"object-literal-sort-keys": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"prefer-const": true,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"radix": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"unified-signatures": true,
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
"no-output-on-prefix": true,
|
||||
"use-input-property-decorator": true,
|
||||
"use-output-property-decorator": true,
|
||||
"use-host-property-decorator": true,
|
||||
"no-input-rename": true,
|
||||
"no-output-rename": true,
|
||||
"use-life-cycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"component-class-suffix": true,
|
||||
"directive-class-suffix": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
Attribute VB_Name = "Module1"
|
||||
Sub ExtractHL()
|
||||
Dim HL As Hyperlink
|
||||
For Each HL In ActiveSheet.Hyperlinks
|
||||
Set c = HL.Parent.TopLeftCell
|
||||
If Not IsNull(c) Then
|
||||
Set c = ActiveSheet.Cells(c.Row, c.Column)
|
||||
Debug.Print (c)
|
||||
c.Value = c.Text + "[" + HL.Address + "#" + HL.SubAddress + "]"
|
||||
Debug.Print (c.Text + HL.Address)
|
||||
End If
|
||||
Next
|
||||
End Sub
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
|
||||
var Excel = require('exceljs');
|
||||
|
||||
|
||||
var inputFile = "./src/app/data/notcheckedin/MSFT ISO 27552 Country Mapping - Merged- Macro.xlsm";
|
||||
var inputFileGdpr = "./src/app/data/notcheckedin/27552 to GDPR.xlsx";
|
||||
|
||||
var importer = require("./importer");
|
||||
|
||||
var keepNWords = function (input, n) {
|
||||
var frags = input.split(" ");
|
||||
var result = frags.slice(0, n).join(" ");
|
||||
if (frags.length > n)
|
||||
result += "...";
|
||||
return result;
|
||||
//return input.replace(new RegExp("(([^\s]+\s\s*){" + n + "})(.*)"),"$1…");
|
||||
}
|
||||
|
||||
var removeSpecialCharacters = function (input) {
|
||||
return input.replace(/[^\w\s]/gi, '');
|
||||
}
|
||||
|
||||
var nestDoc = function (doc) {
|
||||
var result = { "children" : [] };
|
||||
|
||||
importer.mergeDoc(doc, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var reduceDoc = function(doc) {
|
||||
var result = { };
|
||||
for (var i = 0; i < doc.length; ++i)
|
||||
{
|
||||
var newCell = doc[i];
|
||||
|
||||
var copywriteWordLimit = 25;
|
||||
newCell.body = keepNWords(newCell.body, copywriteWordLimit);
|
||||
|
||||
if (newCell.id in result)
|
||||
{
|
||||
var lastCell = result[newCell.id];
|
||||
|
||||
//console.log("merge: " + newCell.id);
|
||||
//console.log("with: " + lastCell.id);
|
||||
// merge
|
||||
if (!lastCell.body.includes(newCell.body))
|
||||
{
|
||||
// append body
|
||||
lastCell.body += ' \n' + newCell.body;
|
||||
}
|
||||
|
||||
importer.mergeLinks(newCell, lastCell);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[newCell.id] = newCell;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var keys = Object.keys(result).sort();
|
||||
var objs = keys.map(v => result[v]);
|
||||
return objs;
|
||||
}
|
||||
|
||||
var makeDoc = function (doc, type) {
|
||||
doc.type = type;
|
||||
doc.rev = 1;
|
||||
return doc;
|
||||
}
|
||||
|
||||
var mainMapping = function() {
|
||||
// read from a file
|
||||
var workbook = new Excel.Workbook();
|
||||
return workbook.xlsx.readFile(inputFile)
|
||||
.then(function() {
|
||||
// use workbook
|
||||
var sheetName = 'ISO 27552 Country Mapping';
|
||||
var worksheet = workbook.getWorksheet(sheetName);
|
||||
|
||||
// build iso doc
|
||||
var sectionColumn = worksheet.getColumn(1);
|
||||
var sectionsByRow = { };
|
||||
var unmappedRows = {};
|
||||
|
||||
sectionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
|
||||
// see if this is one of our unmapped/red rows
|
||||
var row = worksheet.getRow(rowNumber);
|
||||
row.eachCell({ includeEmpty: true }, function(cell, col) {
|
||||
if (cell.fill && cell.fill.fgColor && cell.fill.fgColor.argb == 'FFF04C3E')
|
||||
{
|
||||
//console.log(JSON.stringify(cell.fill) + cell.text);
|
||||
unmappedRows[rowNumber] = col;
|
||||
}
|
||||
});
|
||||
|
||||
if (!(rowNumber in unmappedRows))
|
||||
{
|
||||
var section = cell.text.match(/(\d.*)/); // must start with a number
|
||||
if (section)
|
||||
{
|
||||
sectionsByRow[rowNumber] = importer.normalizePath(section[1])
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var allDocs = [];
|
||||
var isoDoc = [];
|
||||
var descriptionColumn = worksheet.getColumn(2);
|
||||
descriptionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
|
||||
var section = sectionsByRow[rowNumber];
|
||||
if (section)
|
||||
{
|
||||
isoDoc.push({
|
||||
id: section,
|
||||
section: section,
|
||||
body: cell.text
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
isoDoc = makeDoc(nestDoc(reduceDoc(isoDoc)), "ISO");
|
||||
//console.log(JSON.stringify(isoDoc, null, 4));
|
||||
allDocs.push(isoDoc);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var headerRow = worksheet.getRow(1);
|
||||
headerRow.eachCell(function(cell, colNumber) {
|
||||
if (colNumber <= 2)
|
||||
return; // skip iso columns.
|
||||
|
||||
console.log('Header ' + colNumber + ' = ' + cell.text);
|
||||
|
||||
var newDoc = [];
|
||||
|
||||
var sectionColumn = worksheet.getColumn(colNumber);
|
||||
sectionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
|
||||
var bodyOverride = null;
|
||||
|
||||
if (rowNumber in unmappedRows && unmappedRows[rowNumber] == colNumber)
|
||||
{
|
||||
// if it's unmapped/red, there is special handling of the text source
|
||||
var row = worksheet.getRow(rowNumber);
|
||||
cell = row.getCell(1); // section column
|
||||
bodyOverride = row.getCell(2).text; // description column
|
||||
}
|
||||
|
||||
|
||||
var section = cell.text.match(/.*\((.*[0-9].*)\).*/);
|
||||
if (section)
|
||||
{
|
||||
var body = cell.text;
|
||||
|
||||
// Look for hyperlink in brackets at end of line
|
||||
var hyperlink = body.match(/\[http(.*)\]/);
|
||||
if (hyperlink)
|
||||
{
|
||||
// format hyperlink
|
||||
hyperlink = 'http' + hyperlink[1];
|
||||
|
||||
// trim hyperlink from the end
|
||||
body = body.slice(0, -(hyperlink.length + 2)); // + 2 for brackets
|
||||
}
|
||||
|
||||
var normalized = importer.normalizePath(section[1]);
|
||||
var sectionText = normalized;
|
||||
if (bodyOverride)
|
||||
{
|
||||
body += " - " + bodyOverride;
|
||||
}
|
||||
|
||||
var isoSection = sectionsByRow[rowNumber];
|
||||
links = isoSection
|
||||
? [ {
|
||||
"id": isoSection,
|
||||
"type": "ISO"
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
newDoc.push({
|
||||
id: normalized,
|
||||
section: sectionText,
|
||||
body: body,
|
||||
hyperlink: hyperlink,
|
||||
links: links
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
newDoc = makeDoc(nestDoc(reduceDoc(newDoc)), removeSpecialCharacters(cell.text));
|
||||
//console.log(JSON.stringify(newDoc, null, 4));
|
||||
allDocs.push(newDoc);
|
||||
});
|
||||
//console.log(JSON.stringify(worksheet.rowCount));
|
||||
|
||||
return allDocs;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var gdprMapping = function() {
|
||||
// read from a file
|
||||
var workbook = new Excel.Workbook();
|
||||
return workbook.xlsx.readFile(inputFileGdpr)
|
||||
.then(function() {
|
||||
// use workbook
|
||||
var sheetName = 'Sheet1';
|
||||
var worksheet = workbook.getWorksheet(sheetName);
|
||||
|
||||
// build iso doc
|
||||
var sectionColumn = worksheet.getColumn(1);
|
||||
var sectionsByRow = { };
|
||||
|
||||
var allDocs = [];
|
||||
var isoDoc = [];
|
||||
var gdprDoc = [];
|
||||
|
||||
var gdprArticlesColIndex = 2;
|
||||
var gdprTextColIndex = 3;
|
||||
var titleColIndex = 4;
|
||||
var descriptionColIndex = 5;
|
||||
|
||||
sectionColumn.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
|
||||
var section = cell.text.match(/(\d.*)/); // must start with a number
|
||||
if (section)
|
||||
{
|
||||
var sectionId = importer.normalizePath(section[1]);
|
||||
sectionsByRow[rowNumber] = sectionId;
|
||||
|
||||
var row = worksheet.getRow(rowNumber);
|
||||
var gdprIds = row.getCell(gdprArticlesColIndex).text.split(",").map(v => v.match(/(\w+)*/g).filter(m => m).join("."));
|
||||
var gdprText = row.getCell(gdprTextColIndex).text.split("\r\n\r\n");
|
||||
|
||||
var gdprZipped = gdprIds.map((v, i, a) => {
|
||||
return {
|
||||
id: v,
|
||||
section: v,
|
||||
body: gdprText[i],
|
||||
links: [ {
|
||||
"id": sectionId,
|
||||
"type": "ISO"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
gdprDoc = gdprDoc.concat(gdprZipped);
|
||||
|
||||
isoDoc.push({
|
||||
id: sectionId,
|
||||
section: sectionId + " - " + row.getCell(titleColIndex).text,
|
||||
body: row.getCell(descriptionColIndex).text
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
isoDoc = makeDoc(nestDoc(reduceDoc(isoDoc)), "ISO");
|
||||
gdprDoc = makeDoc(nestDoc(reduceDoc(gdprDoc)), "GDPR");
|
||||
allDocs.push(isoDoc);
|
||||
allDocs.push(gdprDoc);
|
||||
|
||||
return allDocs;
|
||||
});
|
||||
}
|
||||
|
||||
mainMapping()
|
||||
.then(allDocs => {
|
||||
|
||||
gdprMapping()
|
||||
.then(isoGdpr => {
|
||||
var isoDocMain = allDocs[0];
|
||||
var isoDocGdpr = isoGdpr[0];
|
||||
var gdprDoc = isoGdpr[1];
|
||||
|
||||
allDocs.push(gdprDoc);
|
||||
|
||||
importer.mergeDoc(isoDocGdpr.children, isoDocMain);
|
||||
|
||||
//console.log(JSON.stringify(allDocs, null, 4));
|
||||
|
||||
importer.writeDocs(allDocs);
|
||||
|
||||
importer.exportXlsx(allDocs);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
|
||||
var Excel = require('exceljs');
|
||||
|
||||
var xlsxFile = "./src/app/data/Database.xlsx";
|
||||
var outputFile = "./src/assets/db.json";
|
||||
var outputFile2 = "./docs/assets/db.json"; // write one straight to the bin file so the user doesnt have to run the build pipeline.
|
||||
|
||||
function flatten(nodes, result) {
|
||||
for (var n of nodes)
|
||||
{
|
||||
result.push(n);
|
||||
if (n.children)
|
||||
flatten(n.children, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function exportXlsx(allDocs) {
|
||||
|
||||
var workbook = new Excel.Workbook();
|
||||
|
||||
for (var doc of allDocs)
|
||||
{
|
||||
var sections = flatten(doc.children, []);
|
||||
var worksheet = workbook.addWorksheet(doc.type);
|
||||
|
||||
worksheet.columns = [
|
||||
{ key: 'id' },
|
||||
{ key: 'section' },
|
||||
{ key: 'body' },
|
||||
{ key: 'hyperlink' },
|
||||
{ key: 'isolinks' },
|
||||
];
|
||||
|
||||
worksheet.getColumn('id').values = ["id"].concat(sections.map(v => v.id));
|
||||
worksheet.getColumn('section').values = ["section"].concat(sections.map(v => v.section));
|
||||
worksheet.getColumn('body').values = ["body"].concat(sections.map(v => v.body));
|
||||
worksheet.getColumn('hyperlink').values = ["hyperlink"].concat(sections.map(v => v.hyperlink));
|
||||
worksheet.getColumn('isolinks').values = ["isolinks"].concat(sections.map(v => v.links.map(l => l.id).join(";")));
|
||||
}
|
||||
|
||||
workbook.xlsx.writeFile(xlsxFile)
|
||||
.then(function() {
|
||||
// done
|
||||
});
|
||||
}
|
||||
|
||||
function writeDocs(allDocs) {
|
||||
const fs = require('fs');
|
||||
let data = JSON.stringify(allDocs, null, 4);
|
||||
console.log(data);
|
||||
fs.writeFileSync(outputFile, data);
|
||||
fs.writeFileSync(outputFile2, data);
|
||||
}
|
||||
|
||||
var mergeLinks = function (src, dst) {
|
||||
if (src.links)
|
||||
{
|
||||
if (!dst.links)
|
||||
dst.links = [];
|
||||
|
||||
for (var l of src.links)
|
||||
{
|
||||
if (!dst.links.find(n => n.id == l.id && n.type == l.type))
|
||||
{
|
||||
dst.links.push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var breakPath = function (path) {
|
||||
if (!path)
|
||||
return [];
|
||||
|
||||
var frags = path.split(/\W+/);
|
||||
frags = frags.filter(f => f);
|
||||
return frags;
|
||||
}
|
||||
|
||||
var normalizePath = function (path) {
|
||||
var frags = breakPath(path);
|
||||
var assembled = frags.join(".");
|
||||
return assembled;
|
||||
}
|
||||
|
||||
var findOrCreateSection = function (root, id) {
|
||||
var frags = breakPath(id);
|
||||
var assembled = "";
|
||||
|
||||
for (var f of frags)
|
||||
{
|
||||
if (assembled.length)
|
||||
assembled += '.';
|
||||
assembled += f;
|
||||
|
||||
var child = root.children ? root.children.find(c => c.id == assembled) : null;
|
||||
if (child)
|
||||
root = child;
|
||||
else
|
||||
{
|
||||
var node = {
|
||||
"id": assembled,
|
||||
"frag": f, // sortable fragment
|
||||
"section": assembled,
|
||||
"children": [],
|
||||
"links": []
|
||||
};
|
||||
|
||||
root.children.push(node);
|
||||
root = node;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
var recursiveSort = function (doc) {
|
||||
doc.children.sort((a,b) => {
|
||||
if (!isNaN(a.frag) && !isNaN(b.frag))
|
||||
return parseInt(a.frag) - parseInt(b.frag);
|
||||
|
||||
return a.frag - b.frag;
|
||||
});
|
||||
|
||||
for (var c of doc.children)
|
||||
recursiveSort(c);
|
||||
}
|
||||
|
||||
var mergeDocRecursive = function (src, dst) {
|
||||
for (var d of src)
|
||||
{
|
||||
var node = findOrCreateSection(dst, d.id);
|
||||
|
||||
// copy properties
|
||||
node.id = d.id;
|
||||
node.section = d.section;
|
||||
node.body = d.body;
|
||||
node.hyperlink = d.hyperlink;
|
||||
if (d.links)
|
||||
mergeLinks(d, node);
|
||||
|
||||
if (d.children)
|
||||
mergeDocRecursive(d.children, dst);
|
||||
}
|
||||
}
|
||||
|
||||
var mergeDoc = function (src, dst) {
|
||||
mergeDocRecursive(src, dst);
|
||||
|
||||
recursiveSort(dst);
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
function importXlsx() {
|
||||
var workbook = new Excel.Workbook();
|
||||
return workbook.xlsx.readFile(xlsxFile)
|
||||
.then(function() {
|
||||
var allDocs = [];
|
||||
|
||||
workbook.eachSheet(function(worksheet, sheetId) {
|
||||
console.log(worksheet.name);
|
||||
var idsCol = worksheet.getColumn(1);
|
||||
|
||||
var ids = [];
|
||||
var doc = {
|
||||
"type": worksheet.name,
|
||||
"rev": 1,
|
||||
"children": []
|
||||
};
|
||||
|
||||
var newChildren = [];
|
||||
|
||||
idsCol.eachCell({ includeEmpty: true }, function(cell, rowNumber) {
|
||||
if (rowNumber == 1)
|
||||
return; // skip the header row
|
||||
|
||||
var row = worksheet.getRow(rowNumber);
|
||||
|
||||
var section = row.getCell(2).text;
|
||||
var body = row.getCell(3).text;
|
||||
var hyperlink = row.getCell(4).text;
|
||||
var isolinks = row.getCell(5).text;
|
||||
|
||||
var links = isolinks.split(';').filter(v => v).map(v => { return {
|
||||
"id": v,
|
||||
"type": "ISO"
|
||||
}; });
|
||||
|
||||
newChildren.push({
|
||||
id: cell.text,
|
||||
section: section,
|
||||
body: body.length ? body : undefined,
|
||||
hyperlink: hyperlink.length ? hyperlink : undefined,
|
||||
links: links
|
||||
});
|
||||
});
|
||||
|
||||
mergeDoc(newChildren, doc);
|
||||
allDocs.push(doc);
|
||||
});
|
||||
|
||||
writeDocs(allDocs);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
exportXlsx,
|
||||
writeDocs,
|
||||
mergeDoc,
|
||||
normalizePath,
|
||||
mergeLinks,
|
||||
}
|
||||
|
||||
importXlsx();
|
Загрузка…
Ссылка в новой задаче