mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-20 15:59:51 +02:00
Initial implementation for Issue#2249.
Includes changes to sequence diagram code to enable popup menus and individualized styling of actors Includes unit and e2e tests. Includes updates to the md file for sequencediagrams.
This commit is contained in:
@@ -47,6 +47,9 @@
|
||||
"end" return 'end';
|
||||
"left of" return 'left_of';
|
||||
"right of" return 'right_of';
|
||||
"links" return 'links';
|
||||
"properties" return 'properties';
|
||||
"details" return 'details';
|
||||
"over" return 'over';
|
||||
"note" return 'note';
|
||||
"activate" { this.begin('ID'); return 'activate'; }
|
||||
@@ -110,6 +113,9 @@ statement
|
||||
| 'activate' actor 'NEWLINE' {$$={type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $2};}
|
||||
| 'deactivate' actor 'NEWLINE' {$$={type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $2};}
|
||||
| note_statement 'NEWLINE'
|
||||
| links_statement 'NEWLINE'
|
||||
| properties_statement 'NEWLINE'
|
||||
| details_statement 'NEWLINE'
|
||||
| title text2 'NEWLINE' {$$=[{type:'setTitle', text:$2}]}
|
||||
| 'loop' restOfLine document end
|
||||
{
|
||||
@@ -170,6 +176,27 @@ note_statement
|
||||
$$ = [$3, {type:'addNote', placement:yy.PLACEMENT.OVER, actor:$2.slice(0, 2), text:$4}];}
|
||||
;
|
||||
|
||||
links_statement
|
||||
: 'links' actor text2
|
||||
{
|
||||
$$ = [$2, {type:'addLinks', actor:$2.actor, text:$3}];
|
||||
}
|
||||
;
|
||||
|
||||
properties_statement
|
||||
: 'properties' actor text2
|
||||
{
|
||||
$$ = [$2, {type:'addProperties', actor:$2.actor, text:$3}];
|
||||
}
|
||||
;
|
||||
|
||||
details_statement
|
||||
: 'details' actor text2
|
||||
{
|
||||
$$ = [$2, {type:'addDetails', actor:$2.actor, text:$3}];
|
||||
}
|
||||
;
|
||||
|
||||
spaceList
|
||||
: SPACE spaceList
|
||||
| SPACE
|
||||
|
@@ -29,7 +29,11 @@ export const addActor = function(id, name, description) {
|
||||
name: name,
|
||||
description: description.text,
|
||||
wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap,
|
||||
prevActor: prevActor
|
||||
prevActor: prevActor,
|
||||
links: {},
|
||||
properties: {},
|
||||
actorCnt: null,
|
||||
rectData: null,
|
||||
};
|
||||
if (prevActor && actors[prevActor]) {
|
||||
actors[prevActor].nextActor = id;
|
||||
@@ -206,6 +210,87 @@ export const addNote = function(actor, placement, message) {
|
||||
});
|
||||
};
|
||||
|
||||
export const addLinks = function (actorId, text) {
|
||||
// find the actor
|
||||
const actor = getActor(actorId);
|
||||
// JSON.parse the text
|
||||
try {
|
||||
const links = JSON.parse(text.text);
|
||||
// add the deserialized text to the actor's links field.
|
||||
insertLinks(actor, links);
|
||||
}
|
||||
catch (e) {
|
||||
log.error('error while parsing actor link text', e);
|
||||
}
|
||||
};
|
||||
|
||||
function insertLinks(actor, links) {
|
||||
if (actor.links == null) {
|
||||
actor.links = links;
|
||||
}
|
||||
else {
|
||||
for (let key in links) {
|
||||
actor.links[key] = links[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const addProperties = function (actorId, text) {
|
||||
// find the actor
|
||||
const actor = getActor(actorId);
|
||||
// JSON.parse the text
|
||||
try {
|
||||
const properties = JSON.parse(text.text);
|
||||
// add the deserialized text to the actor's property field.
|
||||
insertProperties(actor, properties);
|
||||
}
|
||||
catch (e) {
|
||||
log.error('error while parsing actor properties text', e);
|
||||
}
|
||||
};
|
||||
|
||||
function insertProperties(actor, properties) {
|
||||
if (actor.properties == null) {
|
||||
actor.properties = properties;
|
||||
}
|
||||
else {
|
||||
for (let key in properties) {
|
||||
actor.properties[key] = properties[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const addDetails = function (actorId, text) {
|
||||
// find the actor
|
||||
const actor = getActor(actorId);
|
||||
const elem = document.getElementById(text.text);
|
||||
|
||||
// JSON.parse the text
|
||||
try {
|
||||
const text = elem.innerHTML;
|
||||
const details = JSON.parse(text);
|
||||
// add the deserialized text to the actor's property field.
|
||||
if (details["properties"]) {
|
||||
insertProperties(actor, details["properties"]);
|
||||
}
|
||||
|
||||
if (details["links"]) {
|
||||
insertLinks(actor, details["links"]);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
log.error('error while parsing actor details text', e);
|
||||
}
|
||||
};
|
||||
|
||||
export const getActorProperty = function (actor, key) {
|
||||
if (typeof actor !== 'undefined' && typeof actor.properties !== 'undefined') {
|
||||
return actor.properties[key];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const setTitle = function(titleWrap) {
|
||||
title = titleWrap.text;
|
||||
titleWrapped = (titleWrap.wrap === undefined && autoWrap()) || !!titleWrap.wrap;
|
||||
@@ -230,6 +315,15 @@ export const apply = function(param) {
|
||||
case 'addNote':
|
||||
addNote(param.actor, param.placement, param.text);
|
||||
break;
|
||||
case 'addLinks':
|
||||
addLinks(param.actor, param.text);
|
||||
break;
|
||||
case 'addProperties':
|
||||
addProperties(param.actor, param.text);
|
||||
break;
|
||||
case 'addDetails':
|
||||
addDetails(param.actor, param.text);
|
||||
break;
|
||||
case 'addMessage':
|
||||
addSignal(param.from, param.to, param.msg, param.signalType);
|
||||
break;
|
||||
@@ -280,6 +374,9 @@ export default {
|
||||
addActor,
|
||||
addMessage,
|
||||
addSignal,
|
||||
addLinks,
|
||||
addDetails,
|
||||
addProperties,
|
||||
autoWrap,
|
||||
setWrap,
|
||||
enableSequenceNumbers,
|
||||
@@ -288,6 +385,7 @@ export default {
|
||||
getActors,
|
||||
getActor,
|
||||
getActorKeys,
|
||||
getActorProperty,
|
||||
getTitle,
|
||||
parseDirective,
|
||||
getConfig: () => configApi.getConfig().sequence,
|
||||
|
@@ -898,6 +898,49 @@ end`;
|
||||
expect(messages[3].message).toBe('');
|
||||
expect(messages[4].message).toBe('I am good thanks!');
|
||||
});
|
||||
|
||||
it('it should handle links', function () {
|
||||
const str = `
|
||||
sequenceDiagram
|
||||
participant a as Alice
|
||||
participant b as Bob
|
||||
participant c as Charlie
|
||||
links a: { "Repo": "https://repo.contoso.com/", "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links b: { "Dashboard": "https://dashboard.contoso.com/" }
|
||||
links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" }
|
||||
`;
|
||||
console.log(str);
|
||||
|
||||
mermaidAPI.parse(str);
|
||||
const actors = parser.yy.getActors();
|
||||
expect(actors.a.links["Repo"]).toBe("https://repo.contoso.com/");
|
||||
expect(actors.b.links["Repo"]).toBe(undefined);
|
||||
expect(actors.a.links["Dashboard"]).toBe("https://dashboard.contoso.com/");
|
||||
expect(actors.b.links["Dashboard"]).toBe("https://dashboard.contoso.com/");
|
||||
expect(actors.a.links["On-Call"]).toBe("https://oncall.contoso.com/?svc=alice");
|
||||
expect(actors.c.links["Dashboard"]).toBe(undefined);
|
||||
});
|
||||
|
||||
it('it should handle properties', function () {
|
||||
const str = `
|
||||
sequenceDiagram
|
||||
participant a as Alice
|
||||
participant b as Bob
|
||||
participant c as Charlie
|
||||
properties a: {"class": "internal-service-actor", "icon": "@clock"}
|
||||
properties b: {"class": "external-service-actor", "icon": "@computer"}
|
||||
`;
|
||||
console.log(str);
|
||||
|
||||
mermaidAPI.parse(str);
|
||||
const actors = parser.yy.getActors();
|
||||
expect(actors.a.properties["class"]).toBe("internal-service-actor");
|
||||
expect(actors.b.properties["class"]).toBe("external-service-actor");
|
||||
expect(actors.a.properties["icon"]).toBe("@clock");
|
||||
expect(actors.b.properties["icon"]).toBe("@computer");
|
||||
expect(actors.c.properties["class"]).toBe(undefined);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when checking the bounds in a sequenceDiagram', function() {
|
||||
|
@@ -230,7 +230,9 @@ const drawNote = function(elem, noteModel) {
|
||||
let textElem = drawText(g, textObj);
|
||||
|
||||
let textHeight = Math.round(
|
||||
textElem.map(te => (te._groups || te)[0][0].getBBox().height).reduce((acc, curr) => acc + curr)
|
||||
textElem
|
||||
.map(te => (te._groups || te)[0][0].getBBox().height)
|
||||
.reduce((acc, curr) => acc + curr)
|
||||
);
|
||||
|
||||
rectElem.attr('height', textHeight + 2 * conf.noteMargin);
|
||||
@@ -253,7 +255,7 @@ const noteFont = cnf => {
|
||||
return {
|
||||
fontFamily: cnf.noteFontFamily,
|
||||
fontSize: cnf.noteFontSize,
|
||||
fontWeight: cnf.noteFontWeight
|
||||
fontWeight: cnf.noteFontWeight,
|
||||
};
|
||||
};
|
||||
const actorFont = cnf => {
|
||||
@@ -308,7 +310,7 @@ const drawMessage = function(g, msgModel) {
|
||||
.attr(
|
||||
'd',
|
||||
`M ${startx},${lineStarty} H ${startx +
|
||||
Math.max(conf.width / 2, textWidth / 2)} V ${lineStarty + 25} H ${startx}`
|
||||
Math.max(conf.width / 2, textWidth / 2)} V ${lineStarty + 25} H ${startx}`
|
||||
);
|
||||
} else {
|
||||
totalOffset += conf.boxMargin;
|
||||
@@ -443,6 +445,24 @@ export const drawActors = function(diagram, actors, actorKeys, verticalPos) {
|
||||
bounds.bumpVerticalPos(conf.height);
|
||||
};
|
||||
|
||||
export const drawActorsPopup = function(diagram, actors, actorKeys) {
|
||||
var maxHeight = 0;
|
||||
var maxWidth = 0;
|
||||
for (let i = 0; i < actorKeys.length; i++) {
|
||||
const actor = actors[actorKeys[i]];
|
||||
const minMenuWidth = getRequiredPopupWidth(actor);
|
||||
var menuDimensions = svgDraw.drawPopup(diagram, actor, minMenuWidth, conf, conf.forceMenus);
|
||||
if (menuDimensions.height > maxHeight) {
|
||||
maxHeight = menuDimensions.height;
|
||||
}
|
||||
if (menuDimensions.width + actor.x > maxWidth) {
|
||||
maxWidth = menuDimensions.width + actor.x;
|
||||
}
|
||||
}
|
||||
|
||||
return { maxHeight: maxHeight, maxWidth: maxWidth };
|
||||
};
|
||||
|
||||
export const setConf = function(cnf) {
|
||||
assignWithDepth(conf, cnf);
|
||||
|
||||
@@ -521,6 +541,10 @@ export const draw = function(text, id) {
|
||||
const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages);
|
||||
conf.height = calculateActorMargins(actors, maxMessageWidthPerActor);
|
||||
|
||||
svgDraw.insertComputerIcon(diagram);
|
||||
svgDraw.insertDatabaseIcon(diagram);
|
||||
svgDraw.insertClockIcon(diagram);
|
||||
|
||||
drawActors(diagram, actors, actorKeys, 0);
|
||||
const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor);
|
||||
|
||||
@@ -687,6 +711,9 @@ export const draw = function(text, id) {
|
||||
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos());
|
||||
}
|
||||
|
||||
// only draw popups for the top row of actors.
|
||||
var requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys);
|
||||
|
||||
const { bounds: box } = bounds.getBounds();
|
||||
|
||||
// Adjust line height of actor lines now that the height of the diagram is known
|
||||
@@ -694,12 +721,23 @@ export const draw = function(text, id) {
|
||||
const actorLines = selectAll('#' + id + ' .actor-line');
|
||||
actorLines.attr('y2', box.stopy);
|
||||
|
||||
let height = box.stopy - box.starty + 2 * conf.diagramMarginY;
|
||||
// Make sure the height of the diagram supports long menus.
|
||||
let boxHeight = box.stopy - box.starty;
|
||||
if (boxHeight < requiredBoxSize.maxHeight) {
|
||||
boxHeight = requiredBoxSize.maxHeight;
|
||||
}
|
||||
|
||||
let height = boxHeight + 2 * conf.diagramMarginY;
|
||||
if (conf.mirrorActors) {
|
||||
height = height - conf.boxMargin + conf.bottomMarginAdj;
|
||||
}
|
||||
|
||||
const width = box.stopx - box.startx + 2 * conf.diagramMarginX;
|
||||
// Make sure the width of the diagram supports wide menus.
|
||||
let boxWidth = box.stopx - box.startx;
|
||||
if (boxWidth < requiredBoxSize.maxWidth) {
|
||||
boxWidth = requiredBoxSize.maxWidth;
|
||||
}
|
||||
const width = boxWidth + 2 * conf.diagramMarginX;
|
||||
|
||||
if (title) {
|
||||
diagram
|
||||
@@ -831,6 +869,20 @@ const getMaxMessageWidthPerActor = function(actors, messages) {
|
||||
return maxMessageWidthPerActor;
|
||||
};
|
||||
|
||||
const getRequiredPopupWidth = function(actor) {
|
||||
let requiredPopupWidth = 0;
|
||||
const textFont = actorFont(conf);
|
||||
for (let key in actor.links) {
|
||||
let labelDimensions = utils.calculateTextDimensions(key, textFont);
|
||||
let labelWidth = labelDimensions.width + 2 * conf.wrapPadding + 2 * conf.boxMargin;
|
||||
if (requiredPopupWidth < labelWidth) {
|
||||
requiredPopupWidth = labelWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return requiredPopupWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will calculate the optimal margin for each given actor, for a given
|
||||
* actor->messageWidth map.
|
||||
@@ -1111,6 +1163,7 @@ const calculateLoopBounds = function(messages, actors) {
|
||||
export default {
|
||||
bounds,
|
||||
drawActors,
|
||||
drawActorsPopup,
|
||||
setConf,
|
||||
draw
|
||||
};
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user