Skip to content

Commit

Permalink
Merge branch 'main' into #162-implement-domainStory-to-improve-type-s…
Browse files Browse the repository at this point in the history
…afety
  • Loading branch information
hofstef committed Jan 9, 2025
2 parents 5cab64d + 5900085 commit f44e953
Show file tree
Hide file tree
Showing 26 changed files with 549 additions and 51 deletions.
7 changes: 7 additions & 0 deletions .archlint/layers.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@
"*/replay/**"
],
"mayUseAllBelow": true
},
{
"name": "unsavedChangesReminder",
"include": [
"*/unsavedChangesReminder/**"
],
"mayUseAllBelow": true
}
],
"mayUseAllBelow": true
Expand Down
10 changes: 10 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
HostListener,
OnInit,
ViewChild,
} from '@angular/core';
Expand Down Expand Up @@ -31,6 +32,7 @@ import {
} from './domain/entities/constants';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ModelerService } from './tools/modeler/services/modeler.service';
import { DirtyFlagService } from './domain/services/dirty-flag.service';

@Component({
selector: 'app-root',
Expand Down Expand Up @@ -73,6 +75,7 @@ export class AppComponent implements OnInit, AfterViewInit {
private snackbar: MatSnackBar,
replayService: ReplayService,
private modelerService: ModelerService,
private dirtyFlagService: DirtyFlagService,
) {
this.showSettings$ = new BehaviorSubject(false);
this.showDescription$ = new BehaviorSubject(true);
Expand Down Expand Up @@ -164,4 +167,11 @@ export class AppComponent implements OnInit, AfterViewInit {
this.autosaveService.loadLatestDraft();
this.cd.detectChanges();
}

@HostListener('window:beforeunload', ['$event'])
onWindowClose(event: any): void {
if (this.dirtyFlagService.dirty) {
event.returnValue = true;
}
}
}
5 changes: 4 additions & 1 deletion src/app/tools/export/services/exportUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export function createTitleAndDescriptionSVGElement(

let titleElement = createTitle(title, width);

let descriptionElement = createDescription(description, width);
let descriptionElement = "";
if (description) {
descriptionElement = createDescription(description, width);
}

// to display the title and description in the SVG-file, we need to add a container for our text-elements

Expand Down
17 changes: 13 additions & 4 deletions src/app/tools/export/services/png.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ export class PngService {
return svg;
}

findMostOuterElements(svg: HTMLElement): Box {
findMostOuterElements(
svg: HTMLElement,
includeSpaceForDescription: boolean,
): Box {
let xLeft = 0;
let xRight = 0;
let yUp = 0;
Expand Down Expand Up @@ -141,7 +144,10 @@ export class PngService {
}
}

yUp -= 75; // we need to adjust yUp to have space for the title and description
// we need to adjust yUp to have space for the description if necessary
if (includeSpaceForDescription) {
yUp -= 75;
}

return {
xLeft,
Expand All @@ -158,7 +164,10 @@ export class PngService {
title: string,
withTitle: boolean,
): string {
const box = this.findMostOuterElements(layerBase);
const box = this.findMostOuterElements(
layerBase,
description === undefined,
);
let viewBoxIndex = svg.indexOf('width="');

this.calculateWidthAndHeight(box);
Expand All @@ -176,7 +185,7 @@ export class PngService {
this.height += dynamicHeightOffset;
}

const bounds = this.createBounds(box, dynamicHeightOffset);
const bounds = this.createBounds(box, withTitle ? dynamicHeightOffset : 0);

const dataStart = svg.substring(0, viewBoxIndex);
viewBoxIndex = svg.indexOf('style="');
Expand Down
22 changes: 22 additions & 0 deletions src/app/tools/export/services/spec/testSVG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,25 @@ export const TEST_SVG =
'<!-- created with bpmn-js / http://bpmn.io -->\n' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="596" height=" 300" viewBox="397 49 596 205" version="1.1"><defs><marker id="activity-black-black-6cuehuqlth6cpijekr3zjscku" viewBox="0 0 20 20" refX="11" refY="10" markerWidth="10" markerHeight="10" orient="auto"><path d="M 1 5 L 11 10 L 1 15 Z" style="fill: black; stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000px, 1px; stroke: black;"/></marker></defs><g class="djs-group"><g class="djs-element djs-shape" style = "display:block" transform="translate(387 84)"><g class="djs-visual"><text lineHeight="1.2" class="djs-label" style="font-family: Arial, sans-serif; font-size: 30; font-weight: normal; fill: rgb(0, 0, 0);"><tspan x="8" y="10" font-size="30">&lt; title &gt;</tspan></text><text lineHeight="1.2" class="djs-label" style="font-family: Arial, sans-serif; font-size: 12; font-weight: normal; fill: rgb(0, 0, 0);"><tspan x="8" y="30" font-size="12">&lt; description &gt;</tspan></text></g></g></g><g class="djs-group"><g class="djs-element djs-shape" data-element-id="shape_2224" transform="matrix(1, 0, 0, 1, 403, 223)" style="display: block;"><g class="djs-visual"><svg fill=" black" viewBox="0 0 24 26" xmlns="http://www.w3.org/2000/svg" width="75" height="75"><path d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg><text style="font-family: Arial, sans-serif; font-size: 12px; font-weight: normal; fill: rgb(0, 0, 0);" lineHeight="1.2" class="djs-label"><tspan x="23.5" y="75">actor</tspan></text></g><rect style="fill: none; stroke-opacity: 0; stroke: white; stroke-width: 15px;" class="djs-hit djs-hit-all" x="0" y="0" width="75" height="75"/><rect x="-6" y="-6" width="87" height="87" style="fill: none;" class="djs-outline"/></g></g><g class="djs-group"><g class="djs-element djs-connection" data-element-id="connection_3552" style="display: block;"><g class="djs-visual"><polyline points="480,257 612,244 " style="stroke: black; fill: none; stroke-width: 1.5px; stroke-linejoin: round; marker-end: url(\'#activity-black-black-6cuehuqlth6cpijekr3zjscku\');"/><text style="font-family: Arial, sans-serif; font-size: 11px; font-weight: normal; fill: black;" lineHeight="1.2" wordWrap="break-word" overflowWrap="break-word" hyphens="auto" class="djs-label"><tspan x="564.0625619930923" y="264.56256199309234">test</tspan></text><text style="font-family: Courier; font-size: 150px; font-weight: normal; fill: white;" lineHeight="1.2" position="absolute" class="djs-labelNumber"><tspan x="444" y="254">.</tspan></text><text style="font-family: Courier New; font-size: 50px; font-weight: normal; fill: black;" lineHeight="1.2" position="absolute" class="djs-labelNumber"><tspan x="474" y="257">o</tspan></text><text style="font-family: Arial, sans-serif; font-size: 11px; font-weight: normal; fill: black;" lineHeight="1.2" position="absolute" class="djs-labelNumber"><tspan x="486" y="250">1</tspan></text></g><polyline points="480,257 612,244 " style="fill: none; stroke-opacity: 0; stroke: white; stroke-width: 15px;" class="djs-hit djs-hit-stroke"/><rect x="474" y="238" width="144" height="25" style="fill: none;" class="djs-outline"/></g></g><g class="djs-group"><g class="djs-element djs-shape" data-element-id="shape_4020" transform="matrix(1, 0, 0, 1, 612, 202)" style="display: block;"><g class="djs-visual"><svg fill=" black" xmlns="http://www.w3.org/2000/svg" width="48.75" height="48.75" viewBox="0 0 24 26" x="12.5" y="12.5"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg><text style="font-family: Arial, sans-serif; font-size: 12px; font-weight: normal; fill: rgb(0, 0, 0);" lineHeight="1.2" class="djs-label"><tspan x="6.5" y="74.75">workObject</tspan></text></g><rect style="fill: none; stroke-opacity: 0; stroke: white; stroke-width: 15px;" class="djs-hit djs-hit-all" x="0" y="0" width="75" height="75"/><rect x="-6" y="-6" width="87" height="87" style="fill: none;" class="djs-outline"/></g></g></svg>';

export const MINIMAL_SVG =
'<?xml version="1.0" encoding="utf-8"?>\n' +
'<!-- created with bpmn-js / http://bpmn.io -->\n' +
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="87" height="87" viewBox="727 -33 87 87" version="1.1">' +
' <g class="djs-group">' +
' <g class="djs-element djs-shape" data-element-id="shape_2827" transform="matrix(1, 0, 0, 1, 733, -27)" style="display: block;">' +
' <g class="djs-visual">' +
' <svg fill=" #000000" viewBox="0 0 24 26" xmlns="http://www.w3.org/2000/svg" width="75" height="75">' +
' <path d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z"/>' +
' <path d="M0 0h24v24H0z" fill="none"/>' +
' </svg>' +
' <text style="font-family: Arial, sans-serif; font-size: 12px; font-weight: normal; fill: rgb(0, 0, 0);" lineHeight="1.2" class="djs-label">' +
' <tspan x="33.875" y="75">x</tspan>' +
' </text>' +
' </g>' +
' <rect style="fill: none; stroke-opacity: 0; stroke: white; stroke-width: 15px;" class="djs-hit djs-hit-all" x="0" y="0" width="75" height="75"/>' +
' <rect x="-6" y="-6" width="87" height="87" style="fill: none;" class="djs-outline"/>' +
' </g>' +
' </g>' +
'</svg>';
32 changes: 30 additions & 2 deletions src/app/tools/export/services/svg.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing';
import { SvgService } from 'src/app/tools/export/services/svg.service';
import { ModelerService } from '../../modeler/services/modeler.service';
import { testConfigAndDst } from '../domain/export/configAndDst';
import { TEST_SVG } from './spec/testSVG';
import { MINIMAL_SVG, TEST_SVG } from './spec/testSVG';

describe('SvgService', () => {
let service: SvgService;
Expand Down Expand Up @@ -35,14 +35,15 @@ describe('SvgService', () => {
expect(service).toBeTruthy();
});

it('should create exportable SVG', () => {
it('should create exportable SVG with simple dst', () => {
const svgData = service.createSVGData(
'title',
'description',
testConfigAndDst,
true,
false,
);
expect(svgData).toContain('<?xml version="1.0" encoding="utf-8"?>');
expect(svgData).toContain('<svg xmlns="http://www.w3.org/2000/svg"');
expect(svgData).toContain('"dst":');
expect(svgData).toContain('domainStory:activity');
Expand All @@ -51,4 +52,31 @@ describe('SvgService', () => {
expect(svgData).toContain('<!-- <DST>');
expect(svgData).toContain('</DST> -->');
});

describe('With minimal DST', () => {
beforeEach(() => {
modelerServiceSpy = TestBed.inject(
ModelerService,
) as jasmine.SpyObj<ModelerService>;
modelerServiceSpy.getEncoded.and.returnValue(MINIMAL_SVG);

service = TestBed.inject(SvgService);
});

it('should create exportable SVG with minimal dst', () => {
const svgData = service.createSVGData(
'title',
'description',
testConfigAndDst,
true,
false,
);
expect(svgData).toContain('<?xml version="1.0" encoding="utf-8"?>');
expect(svgData).toContain('<svg xmlns="http://www.w3.org/2000/svg"');
expect(svgData).toContain('"dst":');
expect(svgData).toContain('"domain":');
expect(svgData).toContain('<!-- <DST>');
expect(svgData).toContain('</DST> -->');
});
});
});
8 changes: 6 additions & 2 deletions src/app/tools/export/services/svg.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../domain/export/exportConstants';
import { StoryCreatorService } from '../../replay/services/story-creator.service';
import { StorySentence } from '../../replay/domain/storySentence';
import { sanitizeTextForSVGExport } from 'src/app/utils/sanitizer';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -166,7 +167,7 @@ export class SvgService {
private findIndexToInsertData(data: string) {
let insertIndex = data.indexOf('</defs>');
if (insertIndex < 0) {
insertIndex = data.indexOf('version="1.2">') + 14;
insertIndex = data.indexOf('version="1.1">') + 14; // BPMN 8 exports SVG v. 1.1
} else {
insertIndex += 7;
}
Expand Down Expand Up @@ -216,7 +217,10 @@ export class SvgService {
}

private appendDST(data: string, dst: ConfigAndDST): string {
data += '\n<!-- <DST>\n' + JSON.stringify(dst, null, 2) + '\n </DST> -->';
data +=
'\n<!-- <DST>\n' +
sanitizeTextForSVGExport(JSON.stringify(dst, null, 2)) +
'\n </DST> -->';
return data;
}
}
16 changes: 13 additions & 3 deletions src/app/tools/import/directive/dragDrop.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SNACKBAR_ERROR,
} from '../../../domain/entities/constants';
import { MatSnackBar } from '@angular/material/snack-bar';
import { DirtyFlagService } from '../../../domain/services/dirty-flag.service';

@Directive({
standalone: true,
Expand All @@ -16,6 +17,7 @@ export class DragDirective {
constructor(
private importDomainStoryService: ImportDomainStoryService,
private snackbar: MatSnackBar,
private dirtyFlagService: DirtyFlagService,
) {}

@HostListener('dragover', ['$event']) public onDragOver(evt: DragEvent) {
Expand All @@ -36,9 +38,17 @@ export class DragDirective {
this.background = '';

if (evt.dataTransfer?.files[0]) {
this.importDomainStoryService.performDropImport(
evt.dataTransfer.files[0],
);
if (this.dirtyFlagService.dirty) {
this.importDomainStoryService.openUnsavedChangesReminderDialog(() =>
this.importDomainStoryService.performDropImport(
evt.dataTransfer!.files[0],
),
);
} else {
this.importDomainStoryService.performDropImport(
evt.dataTransfer.files[0],
);
}
} else {
this.snackbar.open('Nothing to import', undefined, {
duration: SNACKBAR_DURATION_LONG,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<mat-dialog-content>
<label for="urlInput">
<h2>Import Domain Story:</h2>
<h2>Import Domain Story</h2>
</label>
<mat-form-field class="form-width" color="accent">
<mat-label>URL</mat-label>
Expand Down
23 changes: 20 additions & 3 deletions src/app/tools/import/services/import-domain-story.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ModelerService } from '../../modeler/services/modeler.service';
import { ImportDialogComponent } from '../presentation/import-dialog/import-dialog.component';
import { DomainStory } from '../../../domain/entities/domainStory';
import { isPresent } from '../../../utils/isPresent';
import { UnsavedChangesReminderComponent } from '../../unsavedChangesReminder/presentation/unsavedChangesReminder-dialog/unsaved-changes-reminder/unsaved-changes-reminder.component';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -87,7 +88,6 @@ export class ImportDomainStoryService
performImport(): void {
// @ts-ignore
const file = document.getElementById('import').files[0];

this.import(file, file.name);
this.modelerService.commandStackChanged();
}
Expand All @@ -104,6 +104,14 @@ export class ImportDomainStoryService
this.modelerService.commandStackChanged();
}

importNotDirtyFromUrl(fileUrl: string, isDirty: boolean) {
if (isDirty) {
this.openUnsavedChangesReminderDialog(() => this.importFromUrl(fileUrl));
} else {
this.importFromUrl(fileUrl);
}
}

importFromUrl(fileUrl: string): void {
if (!fileUrl.startsWith('http')) {
this.snackbar.open('Url not valid', undefined, {
Expand Down Expand Up @@ -187,14 +195,23 @@ export class ImportDomainStoryService
return isSupported;
}

openImportFromUrlDialog(): void {
openImportFromUrlDialog(isDirty: boolean): void {
const config = new MatDialogConfig();
config.disableClose = false;
config.autoFocus = true;
config.data = (fileUrl: string) => this.importFromUrl(fileUrl);
config.data = (fileUrl: string) =>
this.importNotDirtyFromUrl(fileUrl, isDirty);
this.dialogService.openDialog(ImportDialogComponent, config);
}

openUnsavedChangesReminderDialog(fn: Function): void {
const config = new MatDialogConfig();
config.disableClose = false;
config.autoFocus = true;
config.data = fn;
this.dialogService.openDialog(UnsavedChangesReminderComponent, config);
}

import(input: Blob, filename: string): DomainStory | null {
// return value is currently only used for tests
const egnSvgPattern = /.*(.egn)(\s*\(\d+\)){0,1}\.svg/;
Expand Down
Loading

0 comments on commit f44e953

Please sign in to comment.