mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-28 07:03:17 +08:00
Merge branch 'develop' of github.com:mermaid-js/mermaid into develop
* 'develop' of github.com:mermaid-js/mermaid: chore: Minor cleanup of imperativeState fix: replace functional approaches with oop chore: fix autogen docs chore(sequence): Update packages/mermaid/src/docs/syntax/sequenceDiagram.md chore(sequence): update doc for actors/participant creation/deletion fix chore: remove unused e2e tests file chore: add e2e test that shows db cleanup problem chore: add e2e test that shows db cleanup problem fix: add imperativeState and replace sequenceDb global variables with it
This commit is contained in:
commit
1d4bb50b32
1
.npmrc
1
.npmrc
@ -1,2 +1,3 @@
|
||||
registry=https://registry.npmjs.org
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
|
@ -10,7 +10,7 @@ interface CypressConfig {
|
||||
type CypressMermaidConfig = MermaidConfig & CypressConfig;
|
||||
|
||||
interface CodeObject {
|
||||
code: string;
|
||||
code: string | string[];
|
||||
mermaid: CypressMermaidConfig;
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ const batchId: string =
|
||||
: Cypress.env('CYPRESS_COMMIT') || Date.now().toString());
|
||||
|
||||
export const mermaidUrl = (
|
||||
graphStr: string,
|
||||
graphStr: string | string[],
|
||||
options: CypressMermaidConfig,
|
||||
api: boolean
|
||||
): string => {
|
||||
@ -82,7 +82,7 @@ export const urlSnapshotTest = (
|
||||
};
|
||||
|
||||
export const renderGraph = (
|
||||
graphStr: string,
|
||||
graphStr: string | string[],
|
||||
options: CypressMermaidConfig = {},
|
||||
api = false
|
||||
): void => {
|
||||
|
@ -930,4 +930,36 @@ context('Sequence diagram', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
context('render after error', () => {
|
||||
it('should render diagram after fixing destroy participant error', () => {
|
||||
cy.on('uncaught:exception', (err) => {
|
||||
return false;
|
||||
});
|
||||
|
||||
renderGraph([
|
||||
`sequenceDiagram
|
||||
Alice->>Bob: Hello Bob, how are you ?
|
||||
Bob->>Alice: Fine, thank you. And you?
|
||||
create participant Carl
|
||||
Alice->>Carl: Hi Carl!
|
||||
create actor D as Donald
|
||||
Carl->>D: Hi!
|
||||
destroy Carl
|
||||
Alice-xCarl: We are too many
|
||||
destroy Bo
|
||||
Bob->>Alice: I agree`,
|
||||
`sequenceDiagram
|
||||
Alice->>Bob: Hello Bob, how are you ?
|
||||
Bob->>Alice: Fine, thank you. And you?
|
||||
create participant Carl
|
||||
Alice->>Carl: Hi Carl!
|
||||
create actor D as Donald
|
||||
Carl->>D: Hi!
|
||||
destroy Carl
|
||||
Alice-xCarl: We are too many
|
||||
destroy Bob
|
||||
Bob->>Alice: I agree`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -131,6 +131,14 @@ sequenceDiagram
|
||||
Bob->>Alice: I agree
|
||||
```
|
||||
|
||||
#### Unfixable actor/participant creation/deletion error
|
||||
|
||||
If an error of the following type occurs when creating or deleting an actor/participant:
|
||||
|
||||
> The destroyed participant **participant-name** does not have an associated destroying message after its declaration. Please check the sequence diagram.
|
||||
|
||||
And fixing diagram code does not get rid of this error and rendering of all other diagrams results in the same error, then you need to update the mermaid version to (v\<MERMAID_RELEASE_VERSION>+).
|
||||
|
||||
### Grouping / Box
|
||||
|
||||
The actor(s) can be grouped in vertical boxes. You can define a color (if not, it will be transparent) and/or a descriptive label using the following notation:
|
||||
|
@ -2,57 +2,60 @@ import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { sanitizeText } from '../common/common.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
getAccTitle,
|
||||
getDiagramTitle,
|
||||
setAccDescription,
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
|
||||
let prevActor = undefined;
|
||||
let actors = {};
|
||||
let createdActors = {};
|
||||
let destroyedActors = {};
|
||||
let boxes = [];
|
||||
let messages = [];
|
||||
const notes = [];
|
||||
let sequenceNumbersEnabled = false;
|
||||
let wrapEnabled;
|
||||
let currentBox = undefined;
|
||||
let lastCreated = undefined;
|
||||
let lastDestroyed = undefined;
|
||||
const state = new ImperativeState(() => ({
|
||||
prevActor: undefined,
|
||||
actors: {},
|
||||
createdActors: {},
|
||||
destroyedActors: {},
|
||||
boxes: [],
|
||||
messages: [],
|
||||
notes: [],
|
||||
sequenceNumbersEnabled: false,
|
||||
wrapEnabled: undefined,
|
||||
currentBox: undefined,
|
||||
lastCreated: undefined,
|
||||
lastDestroyed: undefined,
|
||||
}));
|
||||
|
||||
export const addBox = function (data) {
|
||||
boxes.push({
|
||||
state.records.boxes.push({
|
||||
name: data.text,
|
||||
wrap: (data.wrap === undefined && autoWrap()) || !!data.wrap,
|
||||
fill: data.color,
|
||||
actorKeys: [],
|
||||
});
|
||||
currentBox = boxes.slice(-1)[0];
|
||||
state.records.currentBox = state.records.boxes.slice(-1)[0];
|
||||
};
|
||||
|
||||
export const addActor = function (id, name, description, type) {
|
||||
let assignedBox = currentBox;
|
||||
const old = actors[id];
|
||||
let assignedBox = state.records.currentBox;
|
||||
const old = state.records.actors[id];
|
||||
if (old) {
|
||||
// If already set and trying to set to a new one throw error
|
||||
if (currentBox && old.box && currentBox !== old.box) {
|
||||
if (state.records.currentBox && old.box && state.records.currentBox !== old.box) {
|
||||
throw new Error(
|
||||
'A same participant should only be defined in one Box: ' +
|
||||
old.name +
|
||||
" can't be in '" +
|
||||
old.box.name +
|
||||
"' and in '" +
|
||||
currentBox.name +
|
||||
state.records.currentBox.name +
|
||||
"' at the same time."
|
||||
);
|
||||
}
|
||||
|
||||
// Don't change the box if already
|
||||
assignedBox = old.box ? old.box : currentBox;
|
||||
assignedBox = old.box ? old.box : state.records.currentBox;
|
||||
old.box = assignedBox;
|
||||
|
||||
// Don't allow description nulling
|
||||
@ -69,36 +72,42 @@ export const addActor = function (id, name, description, type) {
|
||||
description = { text: name, wrap: null, type };
|
||||
}
|
||||
|
||||
actors[id] = {
|
||||
state.records.actors[id] = {
|
||||
box: assignedBox,
|
||||
name: name,
|
||||
description: description.text,
|
||||
wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap,
|
||||
prevActor: prevActor,
|
||||
prevActor: state.records.prevActor,
|
||||
links: {},
|
||||
properties: {},
|
||||
actorCnt: null,
|
||||
rectData: null,
|
||||
type: type || 'participant',
|
||||
};
|
||||
if (prevActor && actors[prevActor]) {
|
||||
actors[prevActor].nextActor = id;
|
||||
if (state.records.prevActor && state.records.actors[state.records.prevActor]) {
|
||||
state.records.actors[state.records.prevActor].nextActor = id;
|
||||
}
|
||||
|
||||
if (currentBox) {
|
||||
currentBox.actorKeys.push(id);
|
||||
if (state.records.currentBox) {
|
||||
state.records.currentBox.actorKeys.push(id);
|
||||
}
|
||||
prevActor = id;
|
||||
state.records.prevActor = id;
|
||||
};
|
||||
|
||||
const activationCount = (part) => {
|
||||
let i;
|
||||
let count = 0;
|
||||
for (i = 0; i < messages.length; i++) {
|
||||
if (messages[i].type === LINETYPE.ACTIVE_START && messages[i].from.actor === part) {
|
||||
for (i = 0; i < state.records.messages.length; i++) {
|
||||
if (
|
||||
state.records.messages[i].type === LINETYPE.ACTIVE_START &&
|
||||
state.records.messages[i].from.actor === part
|
||||
) {
|
||||
count++;
|
||||
}
|
||||
if (messages[i].type === LINETYPE.ACTIVE_END && messages[i].from.actor === part) {
|
||||
if (
|
||||
state.records.messages[i].type === LINETYPE.ACTIVE_END &&
|
||||
state.records.messages[i].from.actor === part
|
||||
) {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
@ -106,7 +115,7 @@ const activationCount = (part) => {
|
||||
};
|
||||
|
||||
export const addMessage = function (idFrom, idTo, message, answer) {
|
||||
messages.push({
|
||||
state.records.messages.push({
|
||||
from: idFrom,
|
||||
to: idTo,
|
||||
message: message.text,
|
||||
@ -137,7 +146,7 @@ export const addSignal = function (
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
messages.push({
|
||||
state.records.messages.push({
|
||||
from: idFrom,
|
||||
to: idTo,
|
||||
message: message.text,
|
||||
@ -149,63 +158,58 @@ export const addSignal = function (
|
||||
};
|
||||
|
||||
export const hasAtLeastOneBox = function () {
|
||||
return boxes.length > 0;
|
||||
return state.records.boxes.length > 0;
|
||||
};
|
||||
|
||||
export const hasAtLeastOneBoxWithTitle = function () {
|
||||
return boxes.some((b) => b.name);
|
||||
return state.records.boxes.some((b) => b.name);
|
||||
};
|
||||
|
||||
export const getMessages = function () {
|
||||
return messages;
|
||||
return state.records.messages;
|
||||
};
|
||||
|
||||
export const getBoxes = function () {
|
||||
return boxes;
|
||||
return state.records.boxes;
|
||||
};
|
||||
export const getActors = function () {
|
||||
return actors;
|
||||
return state.records.actors;
|
||||
};
|
||||
export const getCreatedActors = function () {
|
||||
return createdActors;
|
||||
return state.records.createdActors;
|
||||
};
|
||||
export const getDestroyedActors = function () {
|
||||
return destroyedActors;
|
||||
return state.records.destroyedActors;
|
||||
};
|
||||
export const getActor = function (id) {
|
||||
return actors[id];
|
||||
return state.records.actors[id];
|
||||
};
|
||||
export const getActorKeys = function () {
|
||||
return Object.keys(actors);
|
||||
return Object.keys(state.records.actors);
|
||||
};
|
||||
export const enableSequenceNumbers = function () {
|
||||
sequenceNumbersEnabled = true;
|
||||
state.records.sequenceNumbersEnabled = true;
|
||||
};
|
||||
export const disableSequenceNumbers = function () {
|
||||
sequenceNumbersEnabled = false;
|
||||
state.records.sequenceNumbersEnabled = false;
|
||||
};
|
||||
export const showSequenceNumbers = () => sequenceNumbersEnabled;
|
||||
export const showSequenceNumbers = () => state.records.sequenceNumbersEnabled;
|
||||
|
||||
export const setWrap = function (wrapSetting) {
|
||||
wrapEnabled = wrapSetting;
|
||||
state.records.wrapEnabled = wrapSetting;
|
||||
};
|
||||
|
||||
export const autoWrap = () => {
|
||||
// if setWrap has been called, use that value, otherwise use the value from the config
|
||||
// TODO: refactor, always use the config value let setWrap update the config value
|
||||
if (wrapEnabled !== undefined) {
|
||||
return wrapEnabled;
|
||||
if (state.records.wrapEnabled !== undefined) {
|
||||
return state.records.wrapEnabled;
|
||||
}
|
||||
return getConfig().sequence.wrap;
|
||||
};
|
||||
|
||||
export const clear = function () {
|
||||
actors = {};
|
||||
createdActors = {};
|
||||
destroyedActors = {};
|
||||
boxes = [];
|
||||
messages = [];
|
||||
sequenceNumbersEnabled = false;
|
||||
state.reset();
|
||||
commonClear();
|
||||
};
|
||||
|
||||
@ -247,7 +251,7 @@ export const parseBoxData = function (str) {
|
||||
}
|
||||
}
|
||||
|
||||
const boxData = {
|
||||
return {
|
||||
color: color,
|
||||
text:
|
||||
title !== undefined
|
||||
@ -262,7 +266,6 @@ export const parseBoxData = function (str) {
|
||||
: undefined
|
||||
: undefined,
|
||||
};
|
||||
return boxData;
|
||||
};
|
||||
|
||||
export const LINETYPE = {
|
||||
@ -321,8 +324,8 @@ export const addNote = function (actor, placement, message) {
|
||||
// eslint-disable-next-line unicorn/prefer-spread
|
||||
const actors = [].concat(actor, actor);
|
||||
|
||||
notes.push(note);
|
||||
messages.push({
|
||||
state.records.notes.push(note);
|
||||
state.records.messages.push({
|
||||
from: actors[0],
|
||||
to: actors[1],
|
||||
message: message.text,
|
||||
@ -414,7 +417,7 @@ function insertProperties(actor, properties) {
|
||||
*
|
||||
*/
|
||||
function boxEnd() {
|
||||
currentBox = undefined;
|
||||
state.records.currentBox = undefined;
|
||||
}
|
||||
|
||||
export const addDetails = function (actorId, text) {
|
||||
@ -468,7 +471,7 @@ export const apply = function (param) {
|
||||
} else {
|
||||
switch (param.type) {
|
||||
case 'sequenceIndex':
|
||||
messages.push({
|
||||
state.records.messages.push({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
message: {
|
||||
@ -484,18 +487,18 @@ export const apply = function (param) {
|
||||
addActor(param.actor, param.actor, param.description, param.draw);
|
||||
break;
|
||||
case 'createParticipant':
|
||||
if (actors[param.actor]) {
|
||||
if (state.records.actors[param.actor]) {
|
||||
throw new Error(
|
||||
"It is not possible to have actors with the same id, even if one is destroyed before the next is created. Use 'AS' aliases to simulate the behavior"
|
||||
);
|
||||
}
|
||||
lastCreated = param.actor;
|
||||
state.records.lastCreated = param.actor;
|
||||
addActor(param.actor, param.actor, param.description, param.draw);
|
||||
createdActors[param.actor] = messages.length;
|
||||
state.records.createdActors[param.actor] = state.records.messages.length;
|
||||
break;
|
||||
case 'destroyParticipant':
|
||||
lastDestroyed = param.actor;
|
||||
destroyedActors[param.actor] = messages.length;
|
||||
state.records.lastDestroyed = param.actor;
|
||||
state.records.destroyedActors[param.actor] = state.records.messages.length;
|
||||
break;
|
||||
case 'activeStart':
|
||||
addSignal(param.actor, undefined, undefined, param.signalType);
|
||||
@ -519,25 +522,28 @@ export const apply = function (param) {
|
||||
addDetails(param.actor, param.text);
|
||||
break;
|
||||
case 'addMessage':
|
||||
if (lastCreated) {
|
||||
if (param.to !== lastCreated) {
|
||||
if (state.records.lastCreated) {
|
||||
if (param.to !== state.records.lastCreated) {
|
||||
throw new Error(
|
||||
'The created participant ' +
|
||||
lastCreated +
|
||||
state.records.lastCreated +
|
||||
' does not have an associated creating message after its declaration. Please check the sequence diagram.'
|
||||
);
|
||||
} else {
|
||||
lastCreated = undefined;
|
||||
state.records.lastCreated = undefined;
|
||||
}
|
||||
} else if (lastDestroyed) {
|
||||
if (param.to !== lastDestroyed && param.from !== lastDestroyed) {
|
||||
} else if (state.records.lastDestroyed) {
|
||||
if (
|
||||
param.to !== state.records.lastDestroyed &&
|
||||
param.from !== state.records.lastDestroyed
|
||||
) {
|
||||
throw new Error(
|
||||
'The destroyed participant ' +
|
||||
lastDestroyed +
|
||||
state.records.lastDestroyed +
|
||||
' does not have an associated destroying message after its declaration. Please check the sequence diagram.'
|
||||
);
|
||||
} else {
|
||||
lastDestroyed = undefined;
|
||||
state.records.lastDestroyed = undefined;
|
||||
}
|
||||
}
|
||||
addSignal(param.from, param.to, param.msg, param.signalType, param.activate);
|
||||
|
@ -83,6 +83,14 @@ sequenceDiagram
|
||||
Bob->>Alice: I agree
|
||||
```
|
||||
|
||||
#### Unfixable actor/participant creation/deletion error
|
||||
|
||||
If an error of the following type occurs when creating or deleting an actor/participant:
|
||||
|
||||
> The destroyed participant **participant-name** does not have an associated destroying message after its declaration. Please check the sequence diagram.
|
||||
|
||||
And fixing diagram code does not get rid of this error and rendering of all other diagrams results in the same error, then you need to update the mermaid version to (v<MERMAID_RELEASE_VERSION>+).
|
||||
|
||||
### Grouping / Box
|
||||
|
||||
The actor(s) can be grouped in vertical boxes. You can define a color (if not, it will be transparent) and/or a descriptive label using the following notation:
|
||||
|
60
packages/mermaid/src/utils/imperativeState.spec.ts
Normal file
60
packages/mermaid/src/utils/imperativeState.spec.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { ImperativeState } from './imperativeState.js';
|
||||
|
||||
describe('createImperativeState', () => {
|
||||
it('should create state with values from initializer', () => {
|
||||
const baz = {
|
||||
flag: false,
|
||||
};
|
||||
|
||||
const state = new ImperativeState(() => ({
|
||||
foo: undefined as number | undefined,
|
||||
bar: [] as string[],
|
||||
baz,
|
||||
}));
|
||||
|
||||
expect(state.records.foo).toBeUndefined();
|
||||
expect(state.records.bar).toEqual([]);
|
||||
expect(state.records.baz).toBe(baz);
|
||||
});
|
||||
|
||||
it('should update records', () => {
|
||||
const state = new ImperativeState(() => ({
|
||||
foo: undefined as number | undefined,
|
||||
bar: [] as string[],
|
||||
baz: {
|
||||
flag: false,
|
||||
},
|
||||
}));
|
||||
|
||||
state.records.foo = 5;
|
||||
state.records.bar = ['hello'];
|
||||
state.records.baz.flag = true;
|
||||
|
||||
expect(state.records.foo).toEqual(5);
|
||||
expect(state.records.bar).toEqual(['hello']);
|
||||
expect(state.records.baz).toEqual({
|
||||
flag: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset records', () => {
|
||||
const state = new ImperativeState(() => ({
|
||||
foo: undefined as number | undefined,
|
||||
bar: [] as string[],
|
||||
baz: {
|
||||
flag: false,
|
||||
},
|
||||
}));
|
||||
|
||||
state.records.foo = 5;
|
||||
state.records.bar = ['hello'];
|
||||
state.records.baz.flag = true;
|
||||
state.reset();
|
||||
|
||||
expect(state.records.foo).toBeUndefined();
|
||||
expect(state.records.bar).toEqual([]);
|
||||
expect(state.records.baz).toEqual({
|
||||
flag: false,
|
||||
});
|
||||
});
|
||||
});
|
37
packages/mermaid/src/utils/imperativeState.ts
Normal file
37
packages/mermaid/src/utils/imperativeState.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Resettable state storage.
|
||||
* @example
|
||||
* ```
|
||||
* const state = new ImperativeState(() => {
|
||||
* foo: undefined as string | undefined,
|
||||
* bar: [] as number[],
|
||||
* baz: 1 as number | undefined,
|
||||
* });
|
||||
*
|
||||
* state.records.foo = "hi";
|
||||
* console.log(state.records.foo); // prints "hi";
|
||||
* state.reset();
|
||||
* console.log(state.records.foo); // prints "default";
|
||||
*
|
||||
* // typeof state.records:
|
||||
* // {
|
||||
* // foo: string | undefined, // actual: undefined
|
||||
* // bar: number[], // actual: []
|
||||
* // baz: number | undefined, // actual: 1
|
||||
* // }
|
||||
* ```
|
||||
*/
|
||||
export class ImperativeState<S extends Record<string, unknown>> {
|
||||
public records: S;
|
||||
|
||||
/**
|
||||
* @param init - Function that creates the default state.
|
||||
*/
|
||||
constructor(private init: () => S) {
|
||||
this.records = this.init();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.records = this.init();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user