Merge branch 'develop' into fix-note-non-html-text

This commit is contained in:
Yari DeWalt
2024-10-29 12:21:52 -07:00
committed by GitHub
34 changed files with 2175 additions and 381 deletions

View File

@@ -19,6 +19,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'xyChart',
'requirement',
'mindmap',
'kanban',
'timeline',
'gitGraph',
'c4',

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
Adding Kanban board, a new diagram type

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: error `mermaid.parse` on an invalid shape, so that it matches the errors thrown by `mermaid.render`

View File

@@ -12,6 +12,7 @@ gantt
gitgraph
gzipped
handDrawn
kanban
knsv
Knut
marginx

View File

@@ -0,0 +1,136 @@
import { imgSnapshotTest } from '../../helpers/util.ts';
describe('Kanban diagram', () => {
it('1: should render a kanban with a single section', () => {
imgSnapshotTest(
`kanban
id1[Todo]
docs[Create Documentation]
docs[Create Blog about the new diagram]
`,
{}
);
});
it('2: should render a kanban with multiple sections', () => {
imgSnapshotTest(
`kanban
id1[Todo]
docs[Create Documentation]
id2
docs[Create Blog about the new diagram]
`,
{}
);
});
it('3: should render a kanban with a single wrapping node', () => {
imgSnapshotTest(
`kanban
id1[Todo]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char, wrapping]
`,
{}
);
});
it('4: should handle the height of a section with a wrapping node at the end', () => {
imgSnapshotTest(
`kanban
id1[Todo]
id2[One line]
id3[Title of diagram is more than 100 chars when user duplicates diagram with 100 char, wrapping]
`,
{}
);
});
it('5: should handle the height of a section with a wrapping node at the top', () => {
imgSnapshotTest(
`kanban
id1[Todo]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char, wrapping]
id3[One line]
`,
{}
);
});
it('6: should handle the height of a section with a wrapping node in the middle', () => {
imgSnapshotTest(
`kanban
id1[Todo]
id2[One line]
id3[Title of diagram is more than 100 chars when user duplicates diagram with 100 char, wrapping]
id4[One line]
`,
{}
);
});
it('6: should handle assigments', () => {
imgSnapshotTest(
`kanban
id1[Todo]
docs[Create Documentation]
id2[In progress]
docs[Create Blog about the new diagram]@{ assigned: 'knsv' }
`,
{}
);
});
it('7: should handle prioritization', () => {
imgSnapshotTest(
`kanban
id2[In progress]
vh[Very High]@{ priority: 'Very High' }
h[High]@{ priority: 'High' }
m[Default priority]
l[Low]@{ priority: 'Low' }
vl[Very Low]@{ priority: 'Very Low' }
`,
{}
);
});
it('7: should handle external tickets', () => {
imgSnapshotTest(
`kanban
id1[Todo]
docs[Create Documentation]
id2[In progress]
docs[Create Blog about the new diagram]@{ ticket: MC-2037 }
`,
{}
);
});
it('8: should handle assignments, prioritization and tickets ids in the same item', () => {
imgSnapshotTest(
`kanban
id2[In progress]
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
`,
{}
);
});
it('10: Full example', () => {
imgSnapshotTest(
`---
config:
kanban:
ticketBaseUrl: 'https://abc123.atlassian.net/browse/#TICKET#'
---
kanban
id1[Todo]
docs[Create Documentation]
docs[Create Blog about the new diagram]
id7[In progress]
id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes. And some more just for the extra flare.]
id8[Design grammar]@{ assigned: 'knsv' }
id9[Ready for deploy]
id10[Ready for test]
id11[Done]
id5[define getData]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: MC-2036, priority: 'Very High'}
id3[Update DB function]@{ ticket: MC-2037, assigned: knsv, priority: 'High' }
id4[Create parsing tests]@{ ticket: MC-2038, assigned: 'K.Sveidqvist', priority: 'High' }
id66[last item]@{ priority: 'Very Low', assigned: 'knsv' }
id12[Can't reproduce]
`,
{}
);
});
});

View File

@@ -34,6 +34,7 @@
/* background: rgb(221, 208, 208); */
/* background: #333; */
font-family: 'Arial';
/* color: white; */
/* font-size: 18px !important; */
}
@@ -83,349 +84,36 @@
</head>
<body>
<div class="flex">
<pre id="diagram" class="mermaid2">
---
title: hello2
config:
look: handDrawn
layout: elk
elk:
<!-- nodePlacementStrategy: INTERACTIVE -->
<!-- mergeEdges: true -->
---
stateDiagram-v2
direction LR
accTitle: An idealized Open Source supply-chain graph
%%
state "🟦 Importer" as author_importer
state "🟥 Supplier, Owner" as author_owner
state "🟨🟥 Maintainer, Author\n🟨 Custodian" as author
state "🟩 Distributor" as repository_distributor
state "🟦 Importer" as language_importer
state "🟦🟨 Packager" as language_packager
state "🟦🟨 OSS Steward" as language_steward
state "🟨 Curator" as language_curator
state "🟩 Distributor" as language_distributor
state "🟦 Contributor" as contributor
state "🟦 Importer" as package_importer
state "🟨 Patcher" as package_patcher
state "🟨🟦 Builder\n🟨🟦 Packager\n🟨🟦 Containerizer" as package_packager
state "🟨 Curator" as package_curator
state "🟩 Distributor" as package_distributor
state "🟦 Importer" as integrator_importer
state "🟥 Supplier, Manufacturer, Owner" as integrator_owner
state "🟦🟨🟥 Integrator, Developer" as integrator_developer
state "🟩🟨 SBOM Redactor\n🟩 Publisher" as integrator_publisher
state "🟦🟨 Builder" as integrator_builder
state "🟨 Deployer" as deployer
state "🟦 Vuln. Checker" as integrator_checker
state "🟩🟨 SBOM Redactor" as redactor
state "🟦 Consumer\n🟦 User" as consumer
state "🟦 Auditor" as auditor_internal
state "🟦 Auditor" as auditor_external
%%
classDef createsSBOM stroke:red,stroke-width:3px;
classDef updatesSBOM stroke:yellow,stroke-width:3px;
classDef assemblesSBOM stroke:yellow,stroke-width:3px;
classDef distributesSBOM stroke:green,stroke-width:3px;
classDef verifiesSBOM stroke:#07f,stroke-width:3px;
%%
class author_importer verifiesSBOM
class author_owner createsSBOM
class manufacturer_owner createsSBOM
class author assemblesSBOM
class package_importer verifiesSBOM
class package_patcher updatesSBOM
class package_packager assemblesSBOM
class package_curator distributesSBOM
class package_distributor distributesSBOM
class language_importer verifiesSBOM
class language_packager assemblesSBOM
class language_steward updatesSBOM
class language_curator distributesSBOM
class language_distributor distributesSBOM
class repository_distributor distributesSBOM
class integrator_importer verifiesSBOM
class integrator_owner createsSBOM
class integrator_developer assemblesSBOM
class integrator_publisher distributesSBOM
class integrator_builder assemblesSBOM
class integrator_checker verifiesSBOM
class deployer assemblesSBOM
class redactor distributesSBOM
class auditor_internal verifiesSBOM
class auditor_external verifiesSBOM
state "Maintainer Environment" as environment_maintainer {
[*] --> author_importer
[*] --> author
author_importer --> author
author_owner --> author
author --> language_packager
}
[*] --> environment_maintainer
state "Language Ecosystem" as ecosystem_lang {
[*] --> language_importer
[*] --> language_steward
[*] --> language_curator
[*] --> language_distributor
language_importer --> language_distributor
language_importer --> language_curator
language_steward --> language_curator
language_curator --> language_distributor
}
language_packager --> ecosystem_lang
ecosystem_lang --> ecosystem_lang
state "Public Collaboration Ecosystem" as ecosystem_repo {
[*] --> repository_distributor
}
author --> ecosystem_repo
ecosystem_repo --> author
repository_distributor --> contributor
contributor --> repository_distributor
state "Package Ecosystem" as ecosystem_package {
[*] --> package_importer
[*] --> package_packager
[*] --> package_patcher
package_importer --> package_patcher
package_importer --> package_packager
package_patcher --> package_packager
package_packager --> package_curator
package_packager --> package_distributor
package_curator --> package_distributor
}
repository_distributor --> ecosystem_package
language_distributor --> ecosystem_package
ecosystem_package --> ecosystem_package
state "Integrator Environment" as environment_integrator {
[*] --> integrator_developer
[*] --> integrator_importer
integrator_importer --> integrator_developer
integrator_owner --> integrator_developer
integrator_builder --> integrator_publisher
integrator_developer --> integrator_checker
integrator_checker --> integrator_developer
auditor_internal --> integrator_developer
integrator_developer --> integrator_builder
integrator_developer --> auditor_internal
}
repository_distributor --> environment_integrator
language_distributor --> environment_integrator
package_distributor --> environment_integrator
state "Production Environment" as environment_prod {
[*] --> deployer
deployer --> redactor
}
integrator_publisher --> [*]
integrator_developer --> environment_prod
integrator_builder --> environment_prod
integrator_publisher --> environment_prod
deployer --> auditor_external
deployer --> consumer
redactor --> consumer
</pre>
<pre id="diagram" class="mermaid2">
---
config:
look: neo
---
flowchart RL
subgraph " "
A5@{ shape: manual-file, label: "a label"}
B5@{ shape: manual-input, label: "a label" }
C5@{ shape: mul-doc, label: "a label" }
D5@{ shape: mul-proc, label: "a label" }
E5@{ shape: paper-tape, label: "a label" }
B3@{ shape: das, label: "a label" }
C3@{ shape: disk, label: "a label" }
D4@{ shape: lin-doc, label: "a label" }
E4@{ shape: loop-limit, label: "a label" }
end
subgraph " "
B6@{ shape: summary, label: "a label" }
C6@{ shape: tag-we-rect, label: "a label" }
D6@{ shape: tag-rect, label: "a label" }
A2@{ shape: fork}
B2@{ shape: hourglass }
C2@{ shape: comment, label: "I am a comment" }
D2@{ shape: bolt }
D3@{ shape: disp, label: "a label" }
C4@{ shape: junction, label: "a label" }
A4@{ shape: extract, label: "a label"}
B52[a fr]@{ shape: fr }
end
subgraph " "
A1@{ shape: text, label: This is a textblock}
B1@{ shape: card, label: "a label" }
C1@{ shape: lined-proc, label: "a label" }
D1@{ shape: start, label: "a label" }
E1@{ shape: stop, label: "a label" }
E2@{ shape: doc, label: "a label" }
A6@{ shape: stored-data, label: "a label"}
A3@{ shape: delay, label: "a label" }
E3@{ shape: div-proc, label: "a label" }
B4[a label]@{ shape: win-pane }
end
</pre>
<pre id="diagram" class="mermaid2">
---
title: hello2
config:
look: handDrawn
elk:
<!-- nodePlacementStrategy: SIMPLE -->
---
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
flowchart TD
A([Start]) -->|go to booking page| B("select
ISBS booking no")
A --> QQ{cancel booking}
A --> RR{no show}
A --> SS{change booking}
B -->C(wmpay_request_payment.request_type= 'partial',
wmpay_request_payment.status= 'paid',
pos_booking.booking_status= partial and 'full_deposit')
style C text-align:left
C -->D{manage booking}
D -->|cancel|E[ระบบแสดงช่องให้กรอกเหตุผล]
E -->F{กดปุ่ม 'cancel' หรือไม่}
F -->|Yes|G[ระบบบันทึกค่าใหม่
และไม่สามารถแก้ไขข้อมูลได้]
F -->|No|H[กดปุ่ม 'close']
H -->|ระบบไม่เปลี่ยนแปลงข้อมูล|Z
G -->|ระบบส่งข้อมูล|I[(POS_database)]
I -->|pos_booking.booking_status='cancel'|Z([End])
D -->|no show|J[ระบบแสดงช่องให้กรอกเหตุผล]
J -->K{กดปุ่ม 'noshow' หรือไม่}
K -->|Yes|L[ระบบสร้างใบเสร็จอัตโนมัติ
Product_id: 439,
ItemName: no show]
style L text-align:left
K -->|No|O[กดปุ่ม 'close']
O -->|ระบบไม่เปลี่ยนแปลงข้อมูล|Z
L -->M[ระบบบันทึกค่าใหม่]
M -->|ระบบส่งข้อมูล|N[(POS_database)]
N -->|pos_booking.booking_status=noshow|Z
</pre>
<pre id="diagram" class="mermaid2">
---
title: hello2
config:
look: handDrawn
layout: dagre
elk:
nodePlacementStrategy: BRANDES_KOEPF
---
flowchart
A --> A
subgraph A
B --> B
subgraph B
C
end
end
</pre
>
<pre id="diagram" class="mermaid2">
---
config:
look: handdrawn
flowchart:
htmlLabels: true
---
flowchart
A[I am a long text, where do I go??? handdrawn - true]
</pre
>
</div>
<div class="flex">
<pre id="diagram" class="mermaid2">
---
config:
flowchart:
htmlLabels: false
---
flowchart
A[I am a long text, where do I go??? classic - false]
</pre
>
<pre id="diagram" class="mermaid2">
---
config:
flowchart:
htmlLabels: true
---
flowchart
A[I am a long text, where do I go??? classic - true]
</pre
>
</div>
<pre id="diagram2" class="mermaid2">
flowchart LR
id1(Start)-->id2(Stop)
style id1 fill:#f9f,stroke:#333,stroke-width:4px
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
</pre>
<pre id="diagram3" class="mermaid2">
flowchart LR
A:::foo & B:::bar --> C:::foobar
classDef foo stroke:#f00
classDef bar stroke:#0f0
classDef ash color:red
class C ash
style C stroke:#00f, fill:black
</pre>
<pre id="diagram4" class="mermaid2">
stateDiagram
A:::foo
B:::bar --> C:::foobar
classDef foo stroke:#f00
classDef bar stroke:#0f0
style C stroke:#00f, fill:black, color:white
kanban
id2[In progress]
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
</pre>
<pre id="diagram4" class="mermaid">
flowchart TB
A@{
label: "aksljhf kasjdh"
}
---
config:
kanban:
ticketBaseUrl: 'https://mermaidchart.atlassian.net/browse/#TICKET#'
# sectionWidth: 300
---
kanban
Todo
[Create Documentation]
docs[Create Blog about the new diagram]
id7[In progress]
id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes. And some more just for the extra flare.]
id9[Ready for deploy]
id8[Design grammar]@{ assigned: 'knsv' }
id10[Ready for test]
id4[Create parsing tests]@{ ticket: MC-2038, assigned: 'K.Sveidqvist', priority: 'High' }
id66[last item]@{ priority: 'Very Low', assigned: 'knsv' }
id11[Done]
id5[define getData]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: MC-2036, priority: 'Very High'}
id3[Update DB function]@{ ticket: MC-2037, assigned: knsv, priority: 'High' }
id12[Can't reproduce]
id3[Weird flickering in Firefox]
</pre>
<script type="module">
import mermaid from './mermaid.esm.mjs';
@@ -439,13 +127,15 @@ flowchart TB
};
mermaid.initialize({
// theme: 'base',
// theme: 'default',
// theme: 'forest',
// handDrawnSeed: 12,
// look: 'handDrawn',
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
// layout: 'dagre',
// layout: 'elk',
// layout: 'fixed',
// htmlLabels: false,
htmlLabels: false,
flowchart: { titleTopMargin: 10 },
// fontFamily: 'Caveat',
// fontFamily: 'Kalam',
@@ -455,8 +145,11 @@ flowchart TB
noteFontFamily: 'courier',
messageFontFamily: 'courier',
},
kanban: {
htmlLabels: false,
},
fontSize: 12,
logLevel: 3,
logLevel: 0,
securityLevel: 'loose',
});
function callback() {

View File

@@ -49,7 +49,7 @@ This matters if you are using base tag settings.
#### Defined in
[packages/mermaid/src/config.type.ts:200](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L200)
[packages/mermaid/src/config.type.ts:201](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L201)
---
@@ -59,7 +59,7 @@ This matters if you are using base tag settings.
#### Defined in
[packages/mermaid/src/config.type.ts:197](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L197)
[packages/mermaid/src/config.type.ts:198](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L198)
---
@@ -121,7 +121,7 @@ should not change unless content is changed.
#### Defined in
[packages/mermaid/src/config.type.ts:201](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L201)
[packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
---
@@ -183,7 +183,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
#### Defined in
[packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
[packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204)
---
@@ -217,7 +217,7 @@ If set to true, ignores legacyMathML.
#### Defined in
[packages/mermaid/src/config.type.ts:196](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L196)
[packages/mermaid/src/config.type.ts:197](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L197)
---
@@ -253,6 +253,16 @@ Defines the seed to be used when using handDrawn look. This is important for the
---
### kanban
`Optional` **kanban**: `KanbanDiagramConfig`
#### Defined in
[packages/mermaid/src/config.type.ts:196](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L196)
---
### layout
`Optional` **layout**: `string`
@@ -310,7 +320,7 @@ Defines which main look to use for the diagram.
#### Defined in
[packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204)
[packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205)
---
@@ -354,7 +364,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:199](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L199)
[packages/mermaid/src/config.type.ts:200](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L200)
---
@@ -394,7 +404,7 @@ The maximum allowed size of the users text diagram
#### Defined in
[packages/mermaid/src/config.type.ts:198](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L198)
[packages/mermaid/src/config.type.ts:199](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L199)
---
@@ -465,7 +475,7 @@ This is useful when you want to control how to handle syntax errors in your appl
#### Defined in
[packages/mermaid/src/config.type.ts:210](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L210)
[packages/mermaid/src/config.type.ts:211](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L211)
---
@@ -518,7 +528,7 @@ You may also use `themeCSS` to override this value.
#### Defined in
[packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
[packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
---

View File

@@ -19,4 +19,4 @@ The `parseError` function will not be called.
#### Defined in
[packages/mermaid/src/types.ts:56](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L56)
[packages/mermaid/src/types.ts:59](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L59)

View File

@@ -18,7 +18,7 @@ The config passed as YAML frontmatter or directives
#### Defined in
[packages/mermaid/src/types.ts:67](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L67)
[packages/mermaid/src/types.ts:70](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L70)
---
@@ -30,4 +30,4 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
#### Defined in
[packages/mermaid/src/types.ts:63](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L63)
[packages/mermaid/src/types.ts:66](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L66)

161
docs/syntax/kanban.md Normal file
View File

@@ -0,0 +1,161 @@
> **Warning**
>
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
>
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/kanban.md](../../packages/mermaid/src/docs/syntax/kanban.md).
# Mermaid Kanban Diagram Documentation
Mermaids Kanban diagram allows you to create visual representations of tasks moving through different stages of a workflow. This guide explains how to use the Kanban diagram syntax, based on the provided example.
## Overview
A Kanban diagram in Mermaid starts with the kanban keyword, followed by the definition of columns (stages) and tasks within those columns.
```mermaid-example
kanban
column1[Column Title]
task1[Task Description]
```
```mermaid
kanban
column1[Column Title]
task1[Task Description]
```
## Defining Columns
Columns represent the different stages in your workflow, such as “Todo,” “In Progress,” “Done,” etc. Each column is defined using a unique identifier and a title enclosed in square brackets.
**Syntax:**
```
columnId[Column Title]
```
- columnId: A unique identifier for the column.
- \[Column Title]: The title displayed on the column header.
Like this `id1[Todo]`
## Adding Tasks to Columns
Tasks are listed under their respective columns with an indentation. Each task also has a unique identifier and a description enclosed in square brackets.
**Syntax:**
```
taskId[Task Description]
```
```
• taskId: A unique identifier for the task.
• [Task Description]: The description of the task.
```
**Example:**
```
docs[Create Documentation]
```
## Adding Metadata to Tasks
You can include additional metadata for each task using the @{ ... } syntax. Metadata can contain key-value pairs like assigned, ticket, priority, etc. This will be rendered added to the rendering of the node.
## Supported Metadata Keys
```
• assigned: Specifies who is responsible for the task.
• ticket: Links the task to a ticket or issue number.
• priority: Indicates the urgency of the task. Allowed values: 'Very High', 'High', 'Low' and 'Very Low'
```
```mermaid-example
kanban
todo[Todo]
id3[Update Database Function]@{ ticket: MC-2037, assigned: 'knsv', priority: 'High' }
```
```mermaid
kanban
todo[Todo]
id3[Update Database Function]@{ ticket: MC-2037, assigned: 'knsv', priority: 'High' }
```
## Configuration Options
You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams tacketBaseUrl. This can be set as in the the following example:
```yaml
---
config:
kanban:
ticketBaseUrl: 'https://yourproject.atlassian.net/browse/#TICKET#'
---
```
When the kanban item has an assigned ticket number the ticket number in the diagram will have a link to an external system where the ticket is defined. The `ticketBaseUrl` sets the base URL to the external system and #TICKET# is replaced with the ticket value from task metadata to create a valid link.
## Full Example
Below is the full Kanban diagram based on the provided example:
```mermaid-example
---
config:
kanban:
ticketBaseUrl: 'https://mermaidchart.atlassian.net/browse/#TICKET#'
---
kanban
Todo
[Create Documentation]
docs[Create Blog about the new diagram]
[In progress]
id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes. And some more just for the extra flare.]
id9[Ready for deploy]
id8[Design grammar]@{ assigned: 'knsv' }
id10[Ready for test]
id4[Create parsing tests]@{ ticket: MC-2038, assigned: 'K.Sveidqvist', priority: 'High' }
id66[last item]@{ priority: 'Very Low', assigned: 'knsv' }
id11[Done]
id5[define getData]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: MC-2036, priority: 'Very High'}
id3[Update DB function]@{ ticket: MC-2037, assigned: knsv, priority: 'High' }
id12[Can't reproduce]
id3[Weird flickering in Firefox]
```
```mermaid
---
config:
kanban:
ticketBaseUrl: 'https://mermaidchart.atlassian.net/browse/#TICKET#'
---
kanban
Todo
[Create Documentation]
docs[Create Blog about the new diagram]
[In progress]
id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes. And some more just for the extra flare.]
id9[Ready for deploy]
id8[Design grammar]@{ assigned: 'knsv' }
id10[Ready for test]
id4[Create parsing tests]@{ ticket: MC-2038, assigned: 'K.Sveidqvist', priority: 'High' }
id66[last item]@{ priority: 'Very Low', assigned: 'knsv' }
id11[Done]
id5[define getData]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: MC-2036, priority: 'Very High'}
id3[Update DB function]@{ ticket: MC-2037, assigned: knsv, priority: 'High' }
id12[Can't reproduce]
id3[Weird flickering in Firefox]
```
In conclusion, creating a Kanban diagram in Mermaid is a straightforward process that effectively visualizes your workflow. Start by using the kanban keyword to initiate the diagram. Define your columns with unique identifiers and titles to represent different stages of your project. Under each column, list your tasks—also with unique identifiers—and provide detailed descriptions as needed. Remember that proper indentation is crucial; tasks must be indented under their parent columns to maintain the correct structure.
You can enhance your diagram by adding optional metadata to tasks using the @{ ... } syntax, which allows you to include additional context such as assignee, ticket numbers, and priority levels. For further customization, utilize the configuration block at the top of your file to set global options like ticketBaseUrl for linking tickets directly from your diagram.
By adhering to these guidelines—ensuring unique identifiers, proper indentation, and utilizing metadata and configuration options—you can create a comprehensive and customized Kanban board that effectively maps out your projects workflow using Mermaid.

View File

@@ -193,6 +193,7 @@ export interface MermaidConfig {
requirement?: RequirementDiagramConfig;
architecture?: ArchitectureDiagramConfig;
mindmap?: MindmapDiagramConfig;
kanban?: KanbanDiagramConfig;
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
sankey?: SankeyDiagramConfig;
@@ -1024,6 +1025,17 @@ export interface MindmapDiagramConfig extends BaseDiagramConfig {
padding?: number;
maxNodeWidth?: number;
}
/**
* The object containing configurations specific for kanban diagrams
*
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "KanbanDiagramConfig".
*/
export interface KanbanDiagramConfig extends BaseDiagramConfig {
padding?: number;
sectionWidth?: number;
ticketBaseUrl?: string;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
* via the `definition` "GitGraphDiagramConfig".

View File

@@ -19,6 +19,7 @@ import errorDiagram from '../diagrams/error/errorDiagram.js';
import flowchartElk from '../diagrams/flowchart/elk/detector.js';
import timeline from '../diagrams/timeline/detector.js';
import mindmap from '../diagrams/mindmap/detector.js';
import kanban from '../diagrams/kanban/detector.js';
import sankey from '../diagrams/sankey/sankeyDetector.js';
import { packet } from '../diagrams/packet/detector.js';
import block from '../diagrams/block/blockDetector.js';
@@ -70,6 +71,7 @@ export const addDiagrams = () => {
// Ordering of detectors is important. The first one to return true will be used.
registerLazyLoadedDiagrams(
c4,
kanban,
classDiagramV2,
classDiagram,
er,

View File

@@ -2,6 +2,7 @@ import { select } from 'd3';
import utils, { getEdgeId } from '../../utils.js';
import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
import common from '../common/common.js';
import { isValidShape, type ShapeID } from '../../rendering-util/rendering-elements/shapes.js';
import type { Node, Edge } from '../../rendering-util/types.js';
import { log } from '../../logger.js';
import * as yaml from 'js-yaml';
@@ -14,7 +15,15 @@ import {
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js';
import type {
FlowVertex,
FlowClass,
FlowSubGraph,
FlowText,
FlowEdge,
FlowLink,
FlowVertexTypeParam,
} from './types.js';
import type { NodeMetaData } from '../../types.js';
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
@@ -53,12 +62,11 @@ export const lookUpDomId = function (id: string) {
/**
* Function called by parser when a node definition has been found
*
*/
export const addVertex = function (
id: string,
textObj: FlowText,
type: 'group',
type: FlowVertexTypeParam,
style: string[],
classes: string[],
dir: string,
@@ -133,14 +141,15 @@ export const addVertex = function (
}
// console.log('yamlData', yamlData);
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
if (doc.shape && (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_'))) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
}
// console.log('yamlData doc', doc);
if (doc?.shape) {
if (doc.shape) {
if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
} else if (!isValidShape(doc.shape)) {
throw new Error(`No such shape: ${doc.shape}.`);
}
vertex.type = doc?.shape;
}
if (doc?.label) {
vertex.text = doc?.label;
}
@@ -816,7 +825,7 @@ export const lex = {
firstGraph,
};
const getTypeFromVertex = (vertex: FlowVertex) => {
const getTypeFromVertex = (vertex: FlowVertex): ShapeID => {
if (vertex.img) {
return 'imageSquare';
}
@@ -832,14 +841,18 @@ const getTypeFromVertex = (vertex: FlowVertex) => {
}
return 'icon';
}
if (vertex.type === 'square') {
return 'squareRect';
switch (vertex.type) {
case 'square':
case undefined:
return 'squareRect';
case 'round':
return 'roundedRect';
case 'ellipse':
// @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976
return 'ellipse';
default:
return vertex.type;
}
if (vertex.type === 'round') {
return 'roundedRect';
}
return vertex.type ?? 'squareRect';
};
const findNode = (nodes: Node[], id: string) => nodes.find((node) => node.id === id);

View File

@@ -197,6 +197,21 @@ describe('when parsing directions', function () {
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is }');
});
it('should error on non-existent shape', function () {
expect(() => {
flow.parser.parse(`flowchart TB
A@{ shape: this-shape-does-not-exist }
`);
}).toThrow('No such shape: this-shape-does-not-exist.');
});
it('should error on internal-only shape', function () {
expect(() => {
// this shape does exist, but it's only supposed to be for internal/backwards compatibility use
flow.parser.parse(`flowchart TB
A@{ shape: rect_left_inv_arrow }
`);
}).toThrow('No such shape: rect_left_inv_arrow. Shape names should be lowercase.');
});
it('Diamond shapes should work as usual', function () {
const res = flow.parser.parse(`flowchart TB
A{This is a label}

View File

@@ -1,3 +1,28 @@
import type { ShapeID } from '../../rendering-util/rendering-elements/shapes.js';
/**
* Valid `type` args to `yy.addVertex` taken from
* `packages/mermaid/src/diagrams/flowchart/parser/flow.jison`
*/
export type FlowVertexTypeParam =
| undefined
| 'square'
| 'doublecircle'
| 'circle'
| 'ellipse'
| 'stadium'
| 'subroutine'
| 'rect'
| 'cylinder'
| 'round'
| 'diamond'
| 'hexagon'
| 'odd'
| 'trapezoid'
| 'inv_trapezoid'
| 'lean_right'
| 'lean_left';
export interface FlowVertex {
classes: string[];
dir?: string;
@@ -10,7 +35,7 @@ export interface FlowVertex {
props?: any;
styles: string[];
text?: string;
type?: string;
type?: ShapeID | FlowVertexTypeParam;
icon?: string;
form?: string;
pos?: 't' | 'b';

View File

@@ -0,0 +1,23 @@
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'kanban';
const detector: DiagramDetector = (txt) => {
return /^\s*kanban/.test(txt);
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./kanban-definition.js');
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@@ -0,0 +1,13 @@
// @ts-ignore: JISON doesn't support types
import parser from './parser/kanban.jison';
import db from './kanbanDb.js';
import renderer from './kanbanRenderer.js';
import styles from './styles.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
export const diagram: DiagramDefinition = {
db,
renderer,
parser,
styles,
};

View File

@@ -0,0 +1,494 @@
// @ts-expect-error No types available for JISON
import { parser as kanban } from './parser/kanban.jison';
import kanbanDB from './kanbanDb.js';
import type { KanbanNode } from '../../rendering-util/types.js';
// Todo fix utils functions for tests
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
describe('when parsing a kanban ', function () {
beforeEach(function () {
kanban.yy = kanbanDB;
kanban.yy.clear();
setLogLevel('trace');
});
describe('hiearchy', function () {
it('KNBN-1 should handle a simple root definition abc122', function () {
const str = `kanban
root`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections.length).toEqual(1);
expect(sections[0].label).toEqual('root');
});
it('KNBN-2 should handle a hierachial kanban definition', function () {
const str = `kanban
root
child1
child2
`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections.length).toEqual(1);
expect(sections[0].label).toEqual('root');
expect(children.length).toEqual(2);
expect(children[0].label).toEqual('child1');
expect(children[1].label).toEqual('child2');
});
/** CATCH case when a lower level comes later, should throw
* a
* b
* c
*/
it('3 should handle a simple root definition with a shape and without an id abc123', function () {
const str = `kanban
(root)`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].label).toEqual('root');
});
it('KNBN-4 should not dsitinguis between deeper hierachial levels in thr kanban definition', function () {
const str = `kanban
root
child1
leaf1
child2`;
// less picky is better
// expect(() => kanban.parse(str)).toThrow(
// 'There can be only one root. No parent could be found for ("fakeRoot")'
// );
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections.length).toBe(1);
expect(children.length).toBe(3);
});
it('5 Multiple sections are ok', function () {
const str = `kanban
section1
section2`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections.length).toBe(2);
expect(sections[0].label).toBe('section1');
expect(sections[1].label).toBe('section2');
// expect(() => kanban.parse(str)).toThrow(
// 'There can be only one root. No parent could be found for ("fakeRoot")'
// );
});
it('KNBN-6 real root in wrong place', function () {
const str = `kanban
root
fakeRoot
realRootWrongPlace`;
expect(() => kanban.parse(str)).toThrow(
'Items without section detected, found section ("fakeRoot")'
);
});
});
describe('nodes', function () {
it('KNBN-7 should handle an id and type for a node definition', function () {
const str = `kanban
root[The root]
`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('The root');
});
it('KNBN-8 should handle an id and type for a node definition', function () {
const str = `kanban
root
theId(child1)`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].label).toEqual('root');
expect(children.length).toEqual(1);
const child = children[0];
expect(child.label).toEqual('child1');
expect(child.id).toEqual('theId');
});
it('KNBN-9 should handle an id and type for a node definition', function () {
const str = `kanban
root
theId(child1)`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].label).toEqual('root');
expect(children.length).toEqual(1);
const child = children[0];
expect(child.label).toEqual('child1');
expect(child.id).toEqual('theId');
});
});
describe('decorations', function () {
it('KNBN-13 should be possible to set an icon for the node', function () {
const str = `kanban
root[The root]
::icon(bomb)
`;
// ::class1 class2
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('The root');
expect(sections[0].icon).toEqual('bomb');
});
it('KNBN-14 should be possible to set classes for the node', function () {
const str = `kanban
root[The root]
:::m-4 p-8
`;
// ::class1 class2
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('The root');
expect(sections[0].cssClasses).toEqual('m-4 p-8');
});
it('KNBN-15 should be possible to set both classes and icon for the node', function () {
const str = `kanban
root[The root]
:::m-4 p-8
::icon(bomb)
`;
// ::class1 class2
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('The root');
expect(sections[0].cssClasses).toEqual('m-4 p-8');
expect(sections[0].icon).toEqual('bomb');
});
it('KNBN-16 should be possible to set both classes and icon for the node', function () {
const str = `kanban
root[The root]
::icon(bomb)
:::m-4 p-8
`;
// ::class1 class2
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('The root');
// expect(sections[0].type).toEqual('rect');
expect(sections[0].cssClasses).toEqual('m-4 p-8');
expect(sections[0].icon).toEqual('bomb');
});
});
describe('descriptions', function () {
it('KNBN-17 should be possible to use node syntax in the descriptions', function () {
const str = `kanban
root["String containing []"]
`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('String containing []');
});
it('KNBN-18 should be possible to use node syntax in the descriptions in children', function () {
const str = `kanban
root["String containing []"]
child1["String containing ()"]
`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('String containing []');
expect(children.length).toEqual(1);
expect(children[0].label).toEqual('String containing ()');
});
it('KNBN-19 should be possible to have a child after a class assignment', function () {
const str = `kanban
root(Root)
Child(Child)
:::hot
a(a)
b[New Stuff]`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('Root');
expect(children.length).toEqual(3);
const item1 = children[0];
const item2 = children[1];
const item3 = children[2];
expect(item1.id).toEqual('Child');
expect(item2.id).toEqual('a');
expect(item3.id).toEqual('b');
});
});
it('KNBN-20 should be possible to have meaningless empty rows in a kanban abc124', function () {
const str = `kanban
root(Root)
Child(Child)
a(a)
b[New Stuff]`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('Root');
expect(children.length).toEqual(3);
const item1 = children[0];
const item2 = children[1];
const item3 = children[2];
expect(item1.id).toEqual('Child');
expect(item2.id).toEqual('a');
expect(item3.id).toEqual('b');
});
it('KNBN-21 should be possible to have comments in a kanban', function () {
const str = `kanban
root(Root)
Child(Child)
a(a)
%% This is a comment
b[New Stuff]`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('Root');
const child = children[0];
expect(child.id).toEqual('Child');
expect(children[1].id).toEqual('a');
expect(children[2].id).toEqual('b');
expect(children.length).toEqual(3);
});
it('KNBN-22 should be possible to have comments at the end of a line', function () {
const str = `kanban
root(Root)
Child(Child)
a(a) %% This is a comment
b[New Stuff]`;
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(sections[0].label).toEqual('Root');
expect(children.length).toEqual(3);
const child1 = children[0];
expect(child1.id).toEqual('Child');
const child2 = children[1];
expect(child2.id).toEqual('a');
const child3 = children[2];
expect(child3.id).toEqual('b');
});
it('KNBN-23 Rows with only spaces should not interfere', function () {
const str = 'kanban\nroot\n A\n \n\n B';
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(children.length).toEqual(2);
const child = children[0];
expect(child.id).toEqual('A');
const child2 = children[1];
expect(child2.id).toEqual('B');
});
it('KNBN-24 Handle rows above the kanban declarations', function () {
const str = '\n \nkanban\nroot\n A\n \n\n B';
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(children.length).toEqual(2);
const child = children[0];
expect(child.id).toEqual('A');
const child2 = children[1];
expect(child2.id).toEqual('B');
});
it('KNBN-25 Handle rows above the kanban declarations, no space', function () {
const str = '\n\n\nkanban\nroot\n A\n \n\n B';
kanban.parse(str);
const data = kanban.yy.getData();
const sections = kanban.yy.getSections();
const children = data.nodes.filter((n: KanbanNode) => n.parentId === sections[0].id);
expect(sections[0].id).toEqual('root');
expect(children.length).toEqual(2);
const child = children[0];
expect(child.id).toEqual('A');
const child2 = children[1];
expect(child2.id).toEqual('B');
});
});
describe('item data data', function () {
beforeEach(function () {
kanban.yy = kanbanDB;
kanban.yy.clear();
setLogLevel('trace');
});
it('KNBN-30 should be possible to set the priority', function () {
let str = `kanban
root
`;
str = `kanban
root@{ priority: high }
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].id).toEqual('root');
expect(sections[0].priority).toEqual('high');
});
it('KNBN-31 should be possible to set the assignment', function () {
const str = `kanban
root@{ assigned: knsv }
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].id).toEqual('root');
expect(sections[0].assigned).toEqual('knsv');
});
it('KNBN-32 should be possible to set the icon', function () {
const str = `kanban
root@{ icon: star }
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].id).toEqual('root');
expect(sections[0].icon).toEqual('star');
});
it('KNBN-33 should be possible to set the icon', function () {
const str = `kanban
root@{ icon: star }
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].id).toEqual('root');
expect(sections[0].icon).toEqual('star');
});
it('KNBN-34 should be possible to set the metadata using multiple lines', function () {
const str = `kanban
root@{
icon: star
assigned: knsv
}
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].id).toEqual('root');
expect(sections[0].icon).toEqual('star');
expect(sections[0].assigned).toEqual('knsv');
});
it('KNBN-35 should be possible to set the metadata using one line', function () {
const str = `kanban
root@{ icon: star, assigned: knsv }
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].id).toEqual('root');
expect(sections[0].icon).toEqual('star');
expect(sections[0].assigned).toEqual('knsv');
});
it('KNBN-36 should be possible to set the label using the new syntax', function () {
const str = `kanban
root@{ icon: star, label: 'fix things' }
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
expect(sections[0].label).toEqual('fix things');
});
it('KNBN-37 should be possible to set the external id', function () {
const str = `kanban
root@{ ticket: MC-1234 }
`;
kanban.parse(str);
const sections = kanban.yy.getSections();
const data = kanban.yy.getData();
expect(sections[0].id).toEqual('root');
expect(sections[0].ticket).toEqual('MC-1234');
});
});

View File

@@ -0,0 +1,251 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { D3Element } from '../../types.js';
import { sanitizeText } from '../../diagrams/common/common.js';
import { log } from '../../logger.js';
import type { Edge, KanbanNode } from '../../rendering-util/types.js';
import defaultConfig from '../../defaultConfig.js';
import type { NodeMetaData } from '../../types.js';
import * as yaml from 'js-yaml';
let nodes: KanbanNode[] = [];
let sections: KanbanNode[] = [];
let cnt = 0;
let elements: Record<number, D3Element> = {};
const clear = () => {
nodes = [];
sections = [];
cnt = 0;
elements = {};
};
/*
* if your level is the section level return null - then you do not belong to a level
* otherwise return the current section
*/
const getSection = (level: number) => {
if (nodes.length === 0) {
// console.log('No nodes');
return null;
}
const sectionLevel = nodes[0].level;
let lastSection = null;
for (let i = nodes.length - 1; i >= 0; i--) {
if (nodes[i].level === sectionLevel && !lastSection) {
lastSection = nodes[i];
// console.log('lastSection found', lastSection);
}
// console.log('HERE', nodes[i].id, level, nodes[i].level, sectionLevel);
if (nodes[i].level < sectionLevel) {
throw new Error('Items without section detected, found section ("' + nodes[i].label + '")');
}
}
if (level === lastSection?.level) {
return null;
}
// No found
return lastSection;
};
const getSections = function () {
return sections;
};
const getData = function () {
const edges = [] as Edge[];
const _nodes: KanbanNode[] = [];
const sections = getSections();
const conf = getConfig();
for (const section of sections) {
const node = {
id: section.id,
label: sanitizeText(section.label ?? '', conf),
isGroup: true,
ticket: section.ticket,
shape: 'kanbanSection',
level: section.level,
look: conf.look,
} satisfies KanbanNode;
_nodes.push(node);
const children = nodes.filter((n) => n.parentId === section.id);
for (const item of children) {
const childNode = {
id: item.id,
parentId: section.id,
label: sanitizeText(item.label ?? '', conf),
isGroup: false,
ticket: item?.ticket,
priority: item?.priority,
assigned: item?.assigned,
icon: item?.icon,
shape: 'kanbanItem',
level: item.level,
rx: 5,
cssStyles: ['text-align: left'],
} satisfies KanbanNode;
_nodes.push(childNode);
}
}
return { nodes: _nodes, edges, other: {}, config: getConfig() };
};
const addNode = (level: number, id: string, descr: string, type: number, shapeData: string) => {
const conf = getConfig();
let padding: number = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
switch (type) {
case nodeType.ROUNDED_RECT:
case nodeType.RECT:
case nodeType.HEXAGON:
padding *= 2;
}
const node: KanbanNode = {
id: sanitizeText(id, conf) || 'kbn' + cnt++,
level,
label: sanitizeText(descr, conf),
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
padding,
isGroup: false,
} satisfies KanbanNode;
if (shapeData !== undefined) {
let yamlData;
// detect if shapeData contains a newline character
// console.log('shapeData', shapeData);
if (!shapeData.includes('\n')) {
// console.log('yamlData shapeData has no new lines', shapeData);
yamlData = '{\n' + shapeData + '\n}';
} else {
// console.log('yamlData shapeData has new lines', shapeData);
yamlData = shapeData + '\n';
}
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
// console.log('yamlData', doc);
if (doc.shape && (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_'))) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
}
if (doc?.shape) {
node.shape = doc?.shape;
}
if (doc?.label) {
node.label = doc?.label;
}
if (doc?.icon) {
node.icon = doc?.icon;
}
if (doc?.assigned) {
node.assigned = doc?.assigned;
}
if (doc?.ticket) {
node.ticket = doc?.ticket;
}
if (doc?.priority) {
node.priority = doc?.priority;
}
}
const section = getSection(level);
if (section) {
// @ts-ignore false positive for section.id
node.parentId = section.id || 'kbn' + cnt++;
} else {
sections.push(node);
}
nodes.push(node);
};
const nodeType = {
DEFAULT: 0,
NO_BORDER: 0,
ROUNDED_RECT: 1,
RECT: 2,
CIRCLE: 3,
CLOUD: 4,
BANG: 5,
HEXAGON: 6,
};
const getType = (startStr: string, endStr: string): number => {
log.debug('In get type', startStr, endStr);
switch (startStr) {
case '[':
return nodeType.RECT;
case '(':
return endStr === ')' ? nodeType.ROUNDED_RECT : nodeType.CLOUD;
case '((':
return nodeType.CIRCLE;
case ')':
return nodeType.CLOUD;
case '))':
return nodeType.BANG;
case '{{':
return nodeType.HEXAGON;
default:
return nodeType.DEFAULT;
}
};
const setElementForId = (id: number, element: D3Element) => {
elements[id] = element;
};
const decorateNode = (decoration?: { class?: string; icon?: string }) => {
if (!decoration) {
return;
}
const config = getConfig();
const node = nodes[nodes.length - 1];
if (decoration.icon) {
node.icon = sanitizeText(decoration.icon, config);
}
if (decoration.class) {
node.cssClasses = sanitizeText(decoration.class, config);
}
};
const type2Str = (type: number) => {
switch (type) {
case nodeType.DEFAULT:
return 'no-border';
case nodeType.RECT:
return 'rect';
case nodeType.ROUNDED_RECT:
return 'rounded-rect';
case nodeType.CIRCLE:
return 'circle';
case nodeType.CLOUD:
return 'cloud';
case nodeType.BANG:
return 'bang';
case nodeType.HEXAGON:
return 'hexgon'; // cspell: disable-line
default:
return 'no-border';
}
};
// Expose logger to grammar
const getLogger = () => log;
const getElementById = (id: number) => elements[id];
const db = {
clear,
addNode,
getSections,
getData,
nodeType,
getType,
setElementForId,
decorateNode,
type2Str,
getLogger,
getElementById,
} as const;
export default db;

View File

@@ -0,0 +1,87 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { DrawDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import type { KanbanDB } from './kanbanTypes.js';
import defaultConfig from '../../defaultConfig.js';
import { insertCluster } from '../../rendering-util/rendering-elements/clusters.js';
import { insertNode, positionNode } from '../../rendering-util/rendering-elements/nodes.js';
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
log.debug('Rendering kanban diagram\n' + text);
const db = diagObj.db as KanbanDB;
const data4Layout = db.getData();
const conf = getConfig();
conf.htmlLabels = false;
const svg = selectSvgElement(id);
// Draw the graph and start with drawing the nodes without proper position
// this gives us the size of the nodes and we can set the positions later
const sectionsElem = svg.append('g');
sectionsElem.attr('class', 'sections');
const nodesElem = svg.append('g');
nodesElem.attr('class', 'items');
const sections = data4Layout.nodes.filter((node) => node.isGroup);
let cnt = 0;
// TODO set padding
const padding = 10;
const sectionObjects = [];
let maxLabelHeight = 25;
for (const section of sections) {
const WIDTH = conf?.kanban?.sectionWidth || 200;
// const top = (-WIDTH * 3) / 2 + 25;
// let y = top;
cnt = cnt + 1;
section.x = WIDTH * cnt + ((cnt - 1) * padding) / 2;
section.width = WIDTH;
section.y = 0;
section.height = WIDTH * 3;
section.rx = 5;
section.ry = 5;
// Todo, use theme variable THEME_COLOR_LIMIT instead of 10
section.cssClasses = section.cssClasses + ' section-' + cnt;
const sectionObj = await insertCluster(sectionsElem, section);
maxLabelHeight = Math.max(maxLabelHeight, sectionObj?.labelBBox?.height);
sectionObjects.push(sectionObj);
}
let i = 0;
for (const section of sections) {
const sectionObj = sectionObjects[i];
i = i + 1;
const WIDTH = conf?.kanban?.sectionWidth || 200;
const top = (-WIDTH * 3) / 2 + maxLabelHeight;
let y = top;
const sectionItems = data4Layout.nodes.filter((node) => node.parentId === section.id);
for (const item of sectionItems) {
item.x = section.x;
item.width = WIDTH - 1.5 * padding;
const nodeEl = await insertNode(nodesElem, item, { config: conf });
const bbox = nodeEl.node().getBBox();
item.y = y + bbox.height / 2;
await positionNode(item);
y = item.y + bbox.height / 2 + padding / 2;
}
const rect = sectionObj.cluster.select('rect');
const height = Math.max(y - top + 3 * padding, 50) + (maxLabelHeight - 25);
rect.attr('height', height);
}
// Setup the view box and size of the svg element
setupGraphViewbox(
undefined,
svg,
conf.mindmap?.padding ?? defaultConfig.kanban.padding,
conf.mindmap?.useMaxWidth ?? defaultConfig.kanban.useMaxWidth
);
};
export default {
draw,
};

View File

@@ -0,0 +1,3 @@
import type kanbanDb from './kanbanDb.js';
export type KanbanDB = typeof kanbanDb;

View File

@@ -0,0 +1,166 @@
/** mermaid
* https://knsv.github.io/mermaid
* (c) 2015 Knut Sveidqvist
* MIT license.
*/
%lex
%options case-insensitive
%{
// Pre-lexer code can go here
%}
%x NODE
%x NSTR
%x NSTR2
%x ICON
%x CLASS
%x shapeData
%x shapeDataStr
%x shapeDataEndBracket
%%
\@\{ {
// console.log('=> shapeData', yytext);
this.pushState("shapeData"); yytext=""; return 'SHAPE_DATA' }
<shapeData>["] {
// console.log('=> shapeDataStr', yytext);
this.pushState("shapeDataStr");
return 'SHAPE_DATA';
}
<shapeDataStr>["] {
// console.log('shapeData <==', yytext);
this.popState(); return 'SHAPE_DATA'}
<shapeDataStr>[^\"]+ {
// console.log('shapeData', yytext);
const re = /\n\s*/g;
yytext = yytext.replace(re,"<br/>");
return 'SHAPE_DATA'}
<shapeData>[^}^"]+ {
// console.log('shapeData', yytext);
return 'SHAPE_DATA';
}
<shapeData>"}" {
// console.log('<== root', yytext)
this.popState();
}
\s*\%\%.* {yy.getLogger().trace('Found comment',yytext); return 'SPACELINE';}
// \%\%[^\n]*\n /* skip comments */
"kanban" {return 'KANBAN';}
":::" { this.begin('CLASS'); }
<CLASS>.+ { this.popState();return 'CLASS'; }
<CLASS>\n { this.popState();}
// [\s]*"::icon(" { this.begin('ICON'); }
"::icon(" { yy.getLogger().trace('Begin icon');this.begin('ICON'); }
[\s]+[\n] {yy.getLogger().trace('SPACELINE');return 'SPACELINE' /* skip all whitespace */ ;}
[\n]+ return 'NL';
<ICON>[^\)]+ { return 'ICON'; }
<ICON>\) {yy.getLogger().trace('end icon');this.popState();}
"-)" { yy.getLogger().trace('Exploding node'); this.begin('NODE');return 'NODE_DSTART'; }
"(-" { yy.getLogger().trace('Cloud'); this.begin('NODE');return 'NODE_DSTART'; }
"))" { yy.getLogger().trace('Explosion Bang'); this.begin('NODE');return 'NODE_DSTART'; }
")" { yy.getLogger().trace('Cloud Bang'); this.begin('NODE');return 'NODE_DSTART'; }
"((" { this.begin('NODE');return 'NODE_DSTART'; }
"{{" { this.begin('NODE');return 'NODE_DSTART'; }
"(" { this.begin('NODE');return 'NODE_DSTART'; }
"[" { this.begin('NODE');return 'NODE_DSTART'; }
[\s]+ return 'SPACELIST' /* skip all whitespace */ ;
// !(-\() return 'NODE_ID';
[^\(\[\n\)\{\}@]+ {return 'NODE_ID';}
<<EOF>> return 'EOF';
<NODE>["][`] { this.begin("NSTR2");}
<NSTR2>[^`"]+ { return "NODE_DESCR";}
<NSTR2>[`]["] { this.popState();}
<NODE>["] { yy.getLogger().trace('Starting NSTR');this.begin("NSTR");}
<NSTR>[^"]+ { yy.getLogger().trace('description:', yytext); return "NODE_DESCR";}
<NSTR>["] {this.popState();}
<NODE>[\)]\) {this.popState();yy.getLogger().trace('node end ))');return "NODE_DEND";}
<NODE>[\)] {this.popState();yy.getLogger().trace('node end )');return "NODE_DEND";}
<NODE>[\]] {this.popState();yy.getLogger().trace('node end ...',yytext);return "NODE_DEND";}
<NODE>"}}" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
<NODE>"(-" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
<NODE>"-)" {this.popState();yy.getLogger().trace('node end (-');return "NODE_DEND";}
<NODE>"((" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
<NODE>"(" {this.popState();yy.getLogger().trace('node end ((');return "NODE_DEND";}
<NODE>[^\)\]\(\}]+ { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
<NODE>.+(?!\(\() { yy.getLogger().trace('Long description:', yytext); return 'NODE_DESCR';}
// [\[] return 'NODE_START';
// .+ return 'TXT' ;
/lex
%start start
%% /* language grammar */
start
// %{ : info document 'EOF' { return yy; } }
: mindMap
| spaceLines mindMap
;
spaceLines
: SPACELINE
| spaceLines SPACELINE
| spaceLines NL
;
mindMap
: KANBAN document { return yy; }
| KANBAN NL document { return yy; }
;
stop
: NL {yy.getLogger().trace('Stop NL ');}
| EOF {yy.getLogger().trace('Stop EOF ');}
| SPACELINE
| stop NL {yy.getLogger().trace('Stop NL2 ');}
| stop EOF {yy.getLogger().trace('Stop EOF2 ');}
;
document
: document statement stop
| statement stop
;
statement
: SPACELIST node shapeData { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type, $3); }
| SPACELIST node { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
| SPACELIST ICON { yy.getLogger().trace('Icon: ',$2);yy.decorateNode({icon: $2}); }
| SPACELIST CLASS { yy.decorateNode({class: $2}); }
| SPACELINE { yy.getLogger().trace('SPACELIST');}
| node shapeData { yy.getLogger().trace('Node: ',$1.id);yy.addNode(0, $1.id, $1.descr, $1.type, $2); }
| node { yy.getLogger().trace('Node: ',$1.id);yy.addNode(0, $1.id, $1.descr, $1.type); }
| ICON { yy.decorateNode({icon: $1}); }
| CLASS { yy.decorateNode({class: $1}); }
| SPACELIST
;
node
:nodeWithId
|nodeWithoutId
;
nodeWithoutId
: NODE_DSTART NODE_DESCR NODE_DEND
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $2, descr: $2, type: yy.getType($1, $3) }; }
;
nodeWithId
: NODE_ID { $$ = { id: $1, descr: $1, type: 0 }; }
| NODE_ID NODE_DSTART NODE_DESCR NODE_DEND
{ yy.getLogger().trace("node found ..", $1); $$ = { id: $1, descr: $3, type: yy.getType($2, $4) }; }
;
shapeData:
shapeData SHAPE_DATA
{ $$ = $1 + $2; }
| SHAPE_DATA
{ $$ = $1; }
;
%%

View File

@@ -0,0 +1,105 @@
```mermaid
kanban
New
Sometimes wrong Shape type is highlighted
In progress
```
```mermaid
kanban
Todo
Create JISON
Update DB function
Create parsing tests
define getData
Create renderer
In progress
Design grammar
```
Adding ID
```mermaid
kanban
id1[Todo]
id2[Create JISON]
id3[Update DB function]
id4[Create parsing tests]
id5[define getData]
id6[Create renderer]
id7[In progress]
id8[Design grammar]
```
Background color for section
```mermaid
kanban
id1[Todo]
id2[Create JISON]
id3[Update DB function]
id4[Create parsing tests]
id5[define getData]
id6[Create renderer]
id7[In progress]
id8[Design grammar]
style n2 stroke:#AA00FF,fill:#E1BEE7
```
Background color for section
```mermaid
kanban
id1[Todo]
id2[Create JISON]
id3[Update DB function]
id4[Create parsing tests]
id5[define getData]
id6[Create renderer]
id7[In progress]
id8[Design grammar]
id2@{
assigned: knsv
icon: heart
priority: high
descr: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
style n1 stroke:#AA00FF,fill:#E1BEE7
```
Background color for section
```mermaid
---
config:
kanban:
showIds: true
fields: [[title],[description][id, assigned]]
---
kanban
id1[Todo]
id2[Create JISON]
id3[Update DB function]
id4[Create parsing tests]
id5[define getData]
id6[Create renderer]
id7[In progress]
id8[Design grammar]
id2@{
assigned: knsv
icon: heart
priority: high
descr: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
style n1 stroke:#AA00FF,fill:#E1BEE7
```
priority - dedicated
link - dedicated

View File

@@ -0,0 +1,109 @@
// @ts-expect-error Incorrect khroma types
import { darken, lighten, isDark } from 'khroma';
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
const genSections: DiagramStylesProvider = (options) => {
let sections = '';
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
options['lineColor' + i] = options['lineColor' + i] || options['cScaleInv' + i];
if (isDark(options['lineColor' + i])) {
options['lineColor' + i] = lighten(options['lineColor' + i], 20);
} else {
options['lineColor' + i] = darken(options['lineColor' + i], 20);
}
}
const adjuster = (color: string, level: number) =>
options.darkMode ? darken(color, level) : lighten(color, level);
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
const sw = '' + (17 - 3 * i);
sections += `
.section-${i - 1} rect, .section-${i - 1} path, .section-${i - 1} circle, .section-${
i - 1
} polygon, .section-${i - 1} path {
fill: ${adjuster(options['cScale' + i], 10)};
stroke: ${adjuster(options['cScale' + i], 10)};
}
.section-${i - 1} text {
fill: ${options['cScaleLabel' + i]};
}
.node-icon-${i - 1} {
font-size: 40px;
color: ${options['cScaleLabel' + i]};
}
.section-edge-${i - 1}{
stroke: ${options['cScale' + i]};
}
.edge-depth-${i - 1}{
stroke-width: ${sw};
}
.section-${i - 1} line {
stroke: ${options['cScaleInv' + i]} ;
stroke-width: 3;
}
.disabled, .disabled circle, .disabled text {
fill: lightgray;
}
.disabled text {
fill: #efefef;
}
.node rect,
.node circle,
.node ellipse,
.node polygon,
.node path {
fill: ${options.background};
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
.kanban-ticket-link {
fill: ${options.background};
stroke: ${options.nodeBorder};
text-decoration: underline;
}
`;
}
return sections;
};
// TODO: These options seem incorrect.
const getStyles: DiagramStylesProvider = (options) =>
`
.edge {
stroke-width: 3;
}
${genSections(options)}
.section-root rect, .section-root path, .section-root circle, .section-root polygon {
fill: ${options.git0};
}
.section-root text {
fill: ${options.gitBranchLabel0};
}
.icon-container {
height:100%;
display: flex;
justify-content: center;
align-items: center;
}
.edge {
fill: none;
}
.cluster-label, .label {
color: ${options.textColor};
fill: ${options.textColor};
}
.kanban-label {
dy: 1em;
alignment-baseline: middle;
text-anchor: middle;
dominant-baseline: middle;
text-align: center;
}
`;
export default getStyles;

View File

@@ -157,6 +157,7 @@ function sidebarSyntax() {
{ text: 'XY Chart 🔥', link: '/syntax/xyChart' },
{ text: 'Block Diagram 🔥', link: '/syntax/block' },
{ text: 'Packet 🔥', link: '/syntax/packet' },
{ text: 'Kanban 🔥', link: '/syntax/kanban' },
{ text: 'Architecture 🔥', link: '/syntax/architecture' },
{ text: 'Other Examples', link: '/syntax/examples' },
],

View File

@@ -0,0 +1,113 @@
# Mermaid Kanban Diagram Documentation
Mermaids Kanban diagram allows you to create visual representations of tasks moving through different stages of a workflow. This guide explains how to use the Kanban diagram syntax, based on the provided example.
## Overview
A Kanban diagram in Mermaid starts with the kanban keyword, followed by the definition of columns (stages) and tasks within those columns.
```mermaid-example
kanban
column1[Column Title]
task1[Task Description]
```
## Defining Columns
Columns represent the different stages in your workflow, such as “Todo,” “In Progress,” “Done,” etc. Each column is defined using a unique identifier and a title enclosed in square brackets.
**Syntax:**
```
columnId[Column Title]
```
- columnId: A unique identifier for the column.
- [Column Title]: The title displayed on the column header.
Like this `id1[Todo]`
## Adding Tasks to Columns
Tasks are listed under their respective columns with an indentation. Each task also has a unique identifier and a description enclosed in square brackets.
**Syntax:**
```
taskId[Task Description]
```
• taskId: A unique identifier for the task.
• [Task Description]: The description of the task.
**Example:**
```
docs[Create Documentation]
```
## Adding Metadata to Tasks
You can include additional metadata for each task using the @{ ... } syntax. Metadata can contain key-value pairs like assigned, ticket, priority, etc. This will be rendered added to the rendering of the node.
## Supported Metadata Keys
• assigned: Specifies who is responsible for the task.
• ticket: Links the task to a ticket or issue number.
• priority: Indicates the urgency of the task. Allowed values: 'Very High', 'High', 'Low' and 'Very Low'
```mermaid-example
kanban
todo[Todo]
id3[Update Database Function]@{ ticket: MC-2037, assigned: 'knsv', priority: 'High' }
```
## Configuration Options
You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams tacketBaseUrl. This can be set as in the the following example:
```yaml
---
config:
kanban:
ticketBaseUrl: 'https://yourproject.atlassian.net/browse/#TICKET#'
---
```
When the kanban item has an assigned ticket number the ticket number in the diagram will have a link to an external system where the ticket is defined. The `ticketBaseUrl` sets the base URL to the external system and #TICKET# is replaced with the ticket value from task metadata to create a valid link.
## Full Example
Below is the full Kanban diagram based on the provided example:
```mermaid-example
---
config:
kanban:
ticketBaseUrl: 'https://mermaidchart.atlassian.net/browse/#TICKET#'
---
kanban
Todo
[Create Documentation]
docs[Create Blog about the new diagram]
[In progress]
id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes. And some more just for the extra flare.]
id9[Ready for deploy]
id8[Design grammar]@{ assigned: 'knsv' }
id10[Ready for test]
id4[Create parsing tests]@{ ticket: MC-2038, assigned: 'K.Sveidqvist', priority: 'High' }
id66[last item]@{ priority: 'Very Low', assigned: 'knsv' }
id11[Done]
id5[define getData]
id2[Title of diagram is more than 100 chars when user duplicates diagram with 100 char]@{ ticket: MC-2036, priority: 'Very High'}
id3[Update DB function]@{ ticket: MC-2037, assigned: knsv, priority: 'High' }
id12[Can't reproduce]
id3[Weird flickering in Firefox]
```
In conclusion, creating a Kanban diagram in Mermaid is a straightforward process that effectively visualizes your workflow. Start by using the kanban keyword to initiate the diagram. Define your columns with unique identifiers and titles to represent different stages of your project. Under each column, list your tasks—also with unique identifiers—and provide detailed descriptions as needed. Remember that proper indentation is crucial; tasks must be indented under their parent columns to maintain the correct structure.
You can enhance your diagram by adding optional metadata to tasks using the @{ ... } syntax, which allows you to include additional context such as assignee, ticket numbers, and priority levels. For further customization, utilize the configuration block at the top of your file to set global options like ticketBaseUrl for linking tickets directly from your diagram.
By adhering to these guidelines—ensuring unique identifiers, proper indentation, and utilizing metadata and configuration options—you can create a comprehensive and customized Kanban board that effectively maps out your projects workflow using Mermaid.

View File

@@ -280,6 +280,117 @@ const roundedWithTitle = async (parent, node) => {
return { cluster: shapeSvg, labelBBox: bbox };
};
const kanbanSection = async (parent, node) => {
log.info('Creating subgraph rect for ', node.id, node);
const siteConfig = getConfig();
const { themeVariables, handDrawnSeed } = siteConfig;
const { clusterBkg, clusterBorder } = themeVariables;
const { labelStyles, nodeStyles, borderStyles, backgroundStyles } = styles2String(node);
// Add outer g element
const shapeSvg = parent
.insert('g')
.attr('class', 'cluster ' + node.cssClasses)
.attr('id', node.id)
.attr('data-look', node.look);
const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
// Create the label and insert it after the rect
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');
const text = await createText(labelEl, node.label, {
style: node.labelStyle,
useHtmlLabels,
isNode: true,
width: node.width,
});
// Get the size of the label
let bbox = text.getBBox();
if (evaluate(siteConfig.flowchart.htmlLabels)) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;
if (node.width <= bbox.width + node.padding) {
node.diff = (width - node.width) / 2 - node.padding;
} else {
node.diff = -node.padding;
}
const height = node.height;
const x = node.x - width / 2;
const y = node.y - height / 2;
log.trace('Data ', node, JSON.stringify(node));
let rect;
if (node.look === 'handDrawn') {
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {
roughness: 0.7,
fill: clusterBkg,
// fill: 'red',
stroke: clusterBorder,
fillWeight: 4,
seed: handDrawnSeed,
});
const roughNode = rc.path(createRoundedRectPathD(x, y, width, height, node.rx), options);
rect = shapeSvg.insert(() => {
log.debug('Rough node insert CXC', roughNode);
return roughNode;
}, ':first-child');
// Should we affect the options instead of doing this?
rect.select('path:nth-child(2)').attr('style', borderStyles.join(';'));
rect.select('path').attr('style', backgroundStyles.join(';').replace('fill', 'stroke'));
} else {
// add the rect
rect = shapeSvg.insert('rect', ':first-child');
// center the rect around its coordinate
rect
.attr('style', nodeStyles)
.attr('rx', node.rx)
.attr('ry', node.ry)
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height);
}
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
labelEl.attr(
'transform',
// This puts the label on top of the box instead of inside it
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
);
if (labelStyles) {
const span = labelEl.select('span');
if (span) {
span.attr('style', labelStyles);
}
}
// Center the label
const rectBox = rect.node().getBBox();
node.offsetX = 0;
node.width = rectBox.width;
node.height = rectBox.height;
// Used by layout engine to position subgraph in parent
node.offsetY = bbox.height - node.padding / 2;
node.intersect = function (point) {
return intersectRect(node, point);
};
return { cluster: shapeSvg, labelBBox: bbox };
};
const divider = (parent, node) => {
const siteConfig = getConfig();
@@ -355,6 +466,7 @@ const shapes = {
roundedWithTitle,
noteGroup,
divider,
kanbanSection,
};
let clusterElems = new Map();

View File

@@ -23,7 +23,7 @@ export async function insertNode(elem: SVGGroup, node: Node, renderOptions: Shap
}
}
const shapeHandler = shapes[(node.shape ?? 'undefined') as keyof typeof shapes];
const shapeHandler = node.shape ? shapes[node.shape] : undefined;
if (!shapeHandler) {
throw new Error(`No such shape: ${node.shape}. Please check your syntax.`);

View File

@@ -58,6 +58,7 @@ import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js';
import { waveRectangle } from './shapes/waveRectangle.js';
import { windowPane } from './shapes/windowPane.js';
import { classBox } from './shapes/classBox.js';
import { kanbanItem } from './shapes/kanbanItem.js';
type ShapeHandler = <T extends SVGGraphicsElement>(
parent: D3Selection<T>,
@@ -476,7 +477,7 @@ const generateShapeMap = () => {
icon,
iconRounded,
imageSquare,
kanbanItem,
anchor,
} as const;
@@ -499,4 +500,8 @@ const generateShapeMap = () => {
export const shapes = generateShapeMap();
export function isValidShape(shape: string): shape is ShapeID {
return shape in shapes;
}
export type ShapeID = keyof typeof shapes;

View File

@@ -0,0 +1,148 @@
import { labelHelper, insertLabel, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { SVG } from '../../../diagram-api/types.js';
import type { Node, KanbanNode, ShapeRenderOptions } from '../../types.js';
import { createRoundedRectPathD } from './roundedRectPath.js';
import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
const colorFromPriority = (priority: KanbanNode['priority']) => {
switch (priority) {
case 'Very High':
return 'red';
case 'High':
return 'orange';
case 'Low':
return 'blue';
case 'Very Low':
return 'lightblue';
}
};
export const kanbanItem = async (parent: SVG, node: Node, { config }: ShapeRenderOptions) => {
const unknownNode = node as unknown;
const kanbanNode = unknownNode as KanbanNode;
const { labelStyles, nodeStyles } = styles2String(kanbanNode);
kanbanNode.labelStyle = labelStyles;
const labelPaddingX = 10;
const orgWidth = kanbanNode.width;
kanbanNode.width = (kanbanNode.width ?? 200) - 10;
const {
shapeSvg,
bbox,
label: labelElTitle,
} = await labelHelper(parent, kanbanNode, getNodeClasses(kanbanNode));
const padding = kanbanNode.padding || 10;
let ticketUrl = '';
let link;
if (kanbanNode.ticket && config?.kanban?.ticketBaseUrl) {
ticketUrl = config?.kanban?.ticketBaseUrl.replace('#TICKET#', kanbanNode.ticket);
link = shapeSvg
.insert('svg:a', ':first-child')
.attr('class', 'kanban-ticket-link')
.attr('xlink:href', ticketUrl)
.attr('target', '_blank');
}
const options = {
useHtmlLabels: kanbanNode.useHtmlLabels,
labelStyle: kanbanNode.labelStyle,
width: kanbanNode.width,
icon: kanbanNode.icon,
img: kanbanNode.img,
padding: kanbanNode.padding,
centerLabel: false,
};
const { label: labelEl, bbox: bbox2 } = await insertLabel(
link ? link : shapeSvg,
kanbanNode.ticket || '',
options
);
const { label: labelElAssigned, bbox: bboxAssigned } = await insertLabel(
shapeSvg,
kanbanNode.assigned || '',
options
);
kanbanNode.width = orgWidth;
const labelPaddingY = 10;
const totalWidth = kanbanNode?.width || 0;
const heightAdj = Math.max(bbox2.height, bboxAssigned.height) / 2;
const totalHeight =
Math.max(bbox.height + labelPaddingY * 2, kanbanNode?.height || 0) + heightAdj;
const x = -totalWidth / 2;
const y = -totalHeight / 2;
labelElTitle.attr(
'transform',
'translate(' + (padding - totalWidth / 2) + ', ' + (-heightAdj - bbox.height / 2) + ')'
);
labelEl.attr(
'transform',
'translate(' + (padding - totalWidth / 2) + ', ' + (-heightAdj + bbox.height / 2) + ')'
);
labelElAssigned.attr(
'transform',
'translate(' +
(padding + totalWidth / 2 - bboxAssigned.width - 2 * labelPaddingX) +
', ' +
(-heightAdj + bbox.height / 2) +
')'
);
let rect;
const { rx, ry } = kanbanNode;
const { cssStyles } = kanbanNode;
if (kanbanNode.look === 'handDrawn') {
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(kanbanNode, {});
const roughNode =
rx || ry
? rc.path(createRoundedRectPathD(x, y, totalWidth, totalHeight, rx || 0), options)
: rc.rectangle(x, y, totalWidth, totalHeight, options);
rect = shapeSvg.insert(() => roughNode, ':first-child');
rect.attr('class', 'basic label-container').attr('style', cssStyles);
} else {
rect = shapeSvg.insert('rect', ':first-child');
rect
.attr('class', 'basic label-container __APA__')
.attr('style', nodeStyles)
.attr('rx', rx)
.attr('ry', ry)
.attr('x', x)
.attr('y', y)
.attr('width', totalWidth)
.attr('height', totalHeight);
if (kanbanNode.priority) {
const line = shapeSvg.append('line', ':first-child');
const lineX = x + 2;
const y1 = y + Math.floor((rx ?? 0) / 2);
const y2 = y + totalHeight - Math.floor((rx ?? 0) / 2);
line
.attr('x1', lineX)
.attr('y1', y1)
.attr('x2', lineX)
.attr('y2', y2)
.attr('stroke-width', '4')
.attr('stroke', colorFromPriority(kanbanNode.priority));
}
}
updateNodeBounds(kanbanNode, rect);
kanbanNode.height = totalHeight;
kanbanNode.intersect = function (point) {
return intersect.rect(kanbanNode, point);
};
return shapeSvg;
};

View File

@@ -10,7 +10,8 @@ import type { D3Selection, Point } from '../../../types.js';
export const labelHelper = async <T extends SVGGraphicsElement>(
parent: D3Selection<T>,
node: Node,
_classes?: string
_classes?: string,
_shapeSvg?: D3Selection<T>
) => {
let cssClasses;
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.htmlLabels);
@@ -21,10 +22,12 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
}
// Add outer g element
const shapeSvg = parent
.insert('g')
.attr('class', cssClasses)
.attr('id', node.domId || node.id);
const shapeSvg = _shapeSvg
? _shapeSvg
: parent
.insert('g')
.attr('class', cssClasses)
.attr('id', node.domId || node.id);
// Create the label and insert it after the rect
const labelEl = shapeSvg
@@ -116,7 +119,56 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
labelEl.insert('rect', ':first-child');
return { shapeSvg, bbox, halfPadding, label: labelEl };
};
export const insertLabel = async (
parent: D3Selection<T>,
label: string,
options: {
labelStyle?: string | undefined;
icon?: boolean | undefined;
img?: string | undefined;
useHtmlLabels?: boolean | undefined;
padding: number;
width?: number | undefined;
centerLabel?: boolean | undefined;
addSvgBackground?: boolean | undefined;
}
) => {
const useHtmlLabels = options.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels);
// Create the label and insert it after the rect
const labelEl = parent.insert('g').attr('class', 'label').attr('style', options.labelStyle);
const text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {
useHtmlLabels,
width: options.width || getConfig()?.flowchart?.wrappingWidth,
style: options.labelStyle,
addSvgBackground: !!options.icon || !!options.img,
});
// Get the size of the label
let bbox = text.getBBox();
const halfPadding = options.padding / 2;
if (evaluate(getConfig()?.flowchart?.htmlLabels)) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
// Center the label
if (useHtmlLabels) {
labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
} else {
labelEl.attr('transform', 'translate(' + 0 + ', ' + -bbox.height / 2 + ')');
}
if (options.centerLabel) {
labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
}
labelEl.insert('rect', ':first-child');
return { shapeSvg: parent, bbox, halfPadding, label: labelEl };
};
export const updateNodeBounds = <T extends SVGGraphicsElement>(
node: Node,
// D3Selection<SVGGElement> is for the roughjs case, D3Selection<T> is for the non-roughjs case

View File

@@ -1,5 +1,6 @@
export type MarkdownWordType = 'normal' | 'strong' | 'em';
import type { MermaidConfig } from '../config.type.js';
import type { ShapeID } from './rendering-elements/shapes.js';
export interface MarkdownWord {
content: string;
type: MarkdownWordType;
@@ -37,7 +38,7 @@ export interface Node {
linkTarget?: string;
tooltip?: string;
padding?: number; //REMOVE?, use from LayoutData.config - Keep, this could be shape specific
shape?: string;
shape?: ShapeID;
isGroup: boolean;
width?: number;
height?: number;
@@ -152,3 +153,12 @@ export interface ShapeRenderOptions {
/** Some shapes render differently if a diagram has a direction `LR` */
dir?: Node['dir'];
}
export interface KanbanNode extends Node {
// Kanban specif data
priority?: 'Very High' | 'High' | 'Medium' | 'Low' | 'Very Low';
ticket?: string;
assigned?: string;
icon?: string;
level: number;
}

View File

@@ -48,6 +48,7 @@ required:
- requirement
- architecture
- mindmap
- kanban
- gitGraph
- c4
- sankey
@@ -279,6 +280,8 @@ properties:
$ref: '#/$defs/ArchitectureDiagramConfig'
mindmap:
$ref: '#/$defs/MindmapDiagramConfig'
kanban:
$ref: '#/$defs/KanbanDiagramConfig'
gitGraph:
$ref: '#/$defs/GitGraphDiagramConfig'
c4:
@@ -964,6 +967,23 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: number
default: 200
KanbanDiagramConfig:
title: Kanban Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
description: The object containing configurations specific for kanban diagrams
type: object
unevaluatedProperties: false
properties:
padding:
type: number
default: 8
sectionWidth:
type: number
default: 200
ticketBaseUrl:
type: string
default: ''
PieDiagramConfig:
title: Pie Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]

View File

@@ -8,6 +8,9 @@ export interface NodeMetaData {
w?: string;
h?: string;
constraint?: 'on' | 'off';
priority: 'Very High' | 'High' | 'Medium' | 'Low' | 'Very Low';
assigned?: string;
ticket?: string;
}
import type { MermaidConfig } from './config.type.js';