diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index 286e52227..ac9daae3a 100644 --- a/cypress/integration/rendering/sequencediagram.spec.js +++ b/cypress/integration/rendering/sequencediagram.spec.js @@ -564,6 +564,7 @@ context('Sequence diagram', () => { links a: {"Repo": "https://www.contoso.com/repo", "Swagger": "https://www.contoso.com/swagger"} links j: {"Repo": "https://www.contoso.com/repo"} links a: {"Dashboard": "https://www.contoso.com/dashboard", "On-Call": "https://www.contoso.com/oncall"} + link a: Contacts @ https://contacts.contoso.com/?contact=alice@contoso.com a->>j: Hello John, how are you? j-->>a: Great! `, diff --git a/docs/sequenceDiagram.md b/docs/sequenceDiagram.md index c0b1cb372..59e8ffc78 100644 --- a/docs/sequenceDiagram.md +++ b/docs/sequenceDiagram.md @@ -409,6 +409,27 @@ sequenceDiagram ## Actor Menus Actors can have popup-menus containing individualized links to external pages. For example, if an actor represented a web service, useful links might include a link to the service health dashboard, repo containing the code for the service, or a wiki page describing the service. + +This can be configured by adding one or more link lines with the format: + + link : @ + +``` +sequenceDiagram + participant Alice + participant John + link Alice: Dashboard @ https://dashboard.contoso.com/alice + link Alice: Wiki @ https://wiki.contoso.com/alice + link John: Dashboard @ https://dashboard.contoso.com/john + link John: Wiki @ https://wiki.contoso.com/john + Alice->>John: Hello John, how are you? + John-->>Alice: Great! + Alice-)John: See you later! +``` + +#### Advanced Menu Syntax +There is an advanced syntax that relies on JSON formatting. If you are comfortable with JSON format, then this exists as well. + This can be configured by adding the links lines with the format: links : @@ -426,38 +447,6 @@ sequenceDiagram Alice-)John: See you later! ``` -## Actor Individualized Styles & Icons - -Actors can have individualized styling including an embedded icon. -This can be configured by adding the properties lines with this format: - - properties : { "class": "", "icon": @ -or- -> - -``` -sequenceDiagram - participant Alice - participant John - properties Alice: {"class": "scheduled-job-actor", "icon": "@clock"} - properties John: {"class": "database-service-actor", "icon": "https://icons.contoso.com/database.svg"} - Alice->>John: Hello John, how are you? - John-->>Alice: Great! - Alice-)John: See you later! -``` - -```mermaid -sequenceDiagram - participant Alice - participant John - properties Alice: {"icon": "@clock"} - properties John: {"icon": "@database"} - Alice->>John: Hello John, how are you? - John-->>Alice: Great! - Alice-)John: See you later! -``` - -Built-in icon names include @clock, @database, @computer. - ## Styling Styling of a sequence diagram is done by defining a number of css classes. During rendering these classes are extracted from the file located at src/themes/sequence.scss diff --git a/src/defaultConfig.js b/src/defaultConfig.js index a67bdae4c..522db85ee 100644 --- a/src/defaultConfig.js +++ b/src/defaultConfig.js @@ -372,7 +372,7 @@ const config = { *| forceMenus | forces actor popup menus to always be visible (to support E2E testing). | Boolean| Required | True, False | * * **Notes:** - * + * * Default value: false. */ forceMenus: false, diff --git a/src/diagrams/sequence/parser/sequenceDiagram.jison b/src/diagrams/sequence/parser/sequenceDiagram.jison index 7130adedb..1ce39e33f 100644 --- a/src/diagrams/sequence/parser/sequenceDiagram.jison +++ b/src/diagrams/sequence/parser/sequenceDiagram.jison @@ -48,6 +48,7 @@ "left of" return 'left_of'; "right of" return 'right_of'; "links" return 'links'; +"link" return 'link'; "properties" return 'properties'; "details" return 'details'; "over" return 'over'; @@ -56,7 +57,7 @@ "deactivate" { this.begin('ID'); return 'deactivate'; } "title" return 'title'; "sequenceDiagram" return 'SD'; -"autonumber" return 'autonumber'; +"autonumber" return 'autonumber'; "," return ','; ";" return 'NEWLINE'; [^\+\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; } @@ -66,8 +67,8 @@ "-->" return 'DOTTED_OPEN_ARROW'; \-[x] return 'SOLID_CROSS'; \-\-[x] return 'DOTTED_CROSS'; -\-[\)] return 'SOLID_POINT'; -\-\-[\)] return 'DOTTED_POINT'; +\-[\)] return 'SOLID_POINT'; +\-\-[\)] return 'DOTTED_POINT'; ":"(?:(?:no)?wrap:)?[^#\n;]+ return 'TXT'; "+" return '+'; "-" return '-'; @@ -114,6 +115,7 @@ statement | 'deactivate' actor 'NEWLINE' {$$={type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $2};} | note_statement 'NEWLINE' | links_statement 'NEWLINE' + | link_statement 'NEWLINE' | properties_statement 'NEWLINE' | details_statement 'NEWLINE' | title text2 'NEWLINE' {$$=[{type:'setTitle', text:$2}]} @@ -183,6 +185,13 @@ links_statement } ; +link_statement + : 'link' actor text2 + { + $$ = [$2, {type:'addALink', actor:$2.actor, text:$3}]; + } + ; + properties_statement : 'properties' actor text2 { diff --git a/src/diagrams/sequence/sequenceDb.js b/src/diagrams/sequence/sequenceDb.js index e78345921..af848d983 100644 --- a/src/diagrams/sequence/sequenceDb.js +++ b/src/diagrams/sequence/sequenceDb.js @@ -218,8 +218,24 @@ export const addLinks = function (actorId, text) { 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); } - catch (e) { +}; + +export const addALink = function (actorId, text) { + // find the actor + const actor = getActor(actorId); + try { + const links = {}; + var sep = text.text.indexOf('@'); + var label = text.text.slice(0, sep - 1).trim(); + var link = text.text.slice(sep + 1).trim(); + + links[label] = link; + // add the deserialized text to the actor's links field. + insertLinks(actor, links); + } catch (e) { log.error('error while parsing actor link text', e); } }; @@ -227,8 +243,7 @@ export const addLinks = function (actorId, text) { function insertLinks(actor, links) { if (actor.links == null) { actor.links = links; - } - else { + } else { for (let key in links) { actor.links[key] = links[key]; } @@ -243,8 +258,7 @@ export const addProperties = function (actorId, text) { const properties = JSON.parse(text.text); // add the deserialized text to the actor's property field. insertProperties(actor, properties); - } - catch (e) { + } catch (e) { log.error('error while parsing actor properties text', e); } }; @@ -252,8 +266,7 @@ export const addProperties = function (actorId, text) { function insertProperties(actor, properties) { if (actor.properties == null) { actor.properties = properties; - } - else { + } else { for (let key in properties) { actor.properties[key] = properties[key]; } @@ -270,15 +283,14 @@ export const addDetails = function (actorId, text) { 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['properties']) { + insertProperties(actor, details['properties']); } - if (details["links"]) { - insertLinks(actor, details["links"]); + if (details['links']) { + insertLinks(actor, details['links']); } - } - catch (e) { + } catch (e) { log.error('error while parsing actor details text', e); } }; @@ -318,6 +330,9 @@ export const apply = function (param) { case 'addLinks': addLinks(param.actor, param.text); break; + case 'addALink': + addALink(param.actor, param.text); + break; case 'addProperties': addProperties(param.actor, param.text); break; diff --git a/src/diagrams/sequence/sequenceDiagram.spec.js b/src/diagrams/sequence/sequenceDiagram.spec.js index 5543a32e6..8c0cc10ab 100644 --- a/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/src/diagrams/sequence/sequenceDiagram.spec.js @@ -908,6 +908,9 @@ 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" } +link a: Endpoint @ https://alice.contoso.com +link a: Swagger @ https://swagger.contoso.com +link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com `; console.log(str); @@ -919,6 +922,9 @@ links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" } 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); + expect(actors.a.links["Endpoint"]).toBe("https://alice.contoso.com"); + expect(actors.a.links["Swagger"]).toBe("https://swagger.contoso.com"); + expect(actors.a.links["Tests"]).toBe("https://tests.contoso.com/?svc=alice@contoso.com"); }); it('it should handle properties', function () { diff --git a/src/diagrams/sequence/sequenceRenderer.js b/src/diagrams/sequence/sequenceRenderer.js index 7bde422eb..a3cd18783 100644 --- a/src/diagrams/sequence/sequenceRenderer.js +++ b/src/diagrams/sequence/sequenceRenderer.js @@ -446,7 +446,7 @@ export const drawActors = function (diagram, actors, actorKeys, verticalPos) { bounds.bumpVerticalPos(conf.height); }; -export const drawActorsPopup = function(diagram, actors, actorKeys) { +export const drawActorsPopup = function (diagram, actors, actorKeys) { var maxHeight = 0; var maxWidth = 0; for (let i = 0; i < actorKeys.length; i++) { @@ -870,7 +870,7 @@ const getMaxMessageWidthPerActor = function (actors, messages) { return maxMessageWidthPerActor; }; -const getRequiredPopupWidth = function(actor) { +const getRequiredPopupWidth = function (actor) { let requiredPopupWidth = 0; const textFont = actorFont(conf); for (let key in actor.links) { @@ -881,8 +881,8 @@ const getRequiredPopupWidth = function(actor) { } } - return requiredPopupWidth; -} + return requiredPopupWidth; +}; /** * This will calculate the optimal margin for each given actor, for a given diff --git a/src/diagrams/sequence/styles.js b/src/diagrams/sequence/styles.js index eddbec17b..c2ebb45fc 100644 --- a/src/diagrams/sequence/styles.js +++ b/src/diagrams/sequence/styles.js @@ -95,6 +95,17 @@ const getStyles = (options) => fill: ${options.activationBkgColor}; stroke: ${options.activationBorderColor}; } + + .actorPopupMenu { + position: absolute; + } + + .actorPopupMenuPanel { + position: absolute; + fill: ${options.actorBkg}; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4)); +} `; export default getStyles; diff --git a/src/diagrams/sequence/svgDraw.js b/src/diagrams/sequence/svgDraw.js index 1850ca93e..405c3514c 100644 --- a/src/diagrams/sequence/svgDraw.js +++ b/src/diagrams/sequence/svgDraw.js @@ -19,11 +19,13 @@ export const drawRect = function (elem, rectData) { }; const sanitizeUrl = function (s) { - return s.replace(/&/g, '&').replace(/ tspan { fill: $activationBkgColor; stroke: $activationBorderColor; } - -.actorPopupMenu { - position: absolute; -} - -.actorPopupMenuPanel { - position: absolute; - fill: $actorBkg; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4)); -}