Adding ticket handling

This commit is contained in:
Knut Sveidqvist 2024-10-10 08:23:58 -07:00
parent 93f2c241b8
commit 290c678dc7
16 changed files with 415 additions and 128 deletions

View File

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

View File

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

View File

@ -84,19 +84,27 @@
<body>
<pre id="diagram4" class="mermaid">
---
config:
kanban:
ticketBaseUrl: 'https://mermaidchart.atlassian.net/browse/#TICKET#'
---
kanban
id1[Todo]
id2[Create JISON]
id3[Update DB function]
id4[Create parsing tests]
id6[Create renderer so that it works in all cases. We also add som extra text here for testing purposes.]
id66[last item]
docs[Create Documentation]
docs[Create Blog about the new diagram]
id7[In progress]
id8[Design grammar]
id9[Ready for deploy]
id10[Ready for test]
id5[define getData]
id11[Done]
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' }
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]
</pre>
<script type="module">

View File

@ -193,6 +193,7 @@ export interface MermaidConfig {
requirement?: RequirementDiagramConfig;
architecture?: ArchitectureDiagramConfig;
mindmap?: MindmapDiagramConfig;
kanban?: KanbanDiagramConfig;
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
sankey?: SankeyDiagramConfig;
@ -1023,6 +1024,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

@ -58,6 +58,12 @@ const config: RequiredDeep<MermaidConfig> = {
tickInterval: undefined,
useWidth: undefined, // can probably be removed since `configKeys` already includes this
},
kanban: {
ticketBaseUrl: '', // can probably be removed since `configKeys` already includes this
padding: 8,
sectionWidth: 200,
...defaultConfigJson.kanban,
},
c4: {
...defaultConfigJson.c4,
useWidth: undefined,

View File

@ -380,3 +380,89 @@ root
expect(child2.nodeId).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].nodeId).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].nodeId).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].nodeId).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].nodeId).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].nodeId).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].nodeId).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].descr).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();
expect(sections[0].nodeId).toEqual('root');
expect(sections[0].ticket).toEqual('MC-1234');
});
});

View File

@ -2,12 +2,14 @@ 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 { KanbanNode } from './kanbanTypes.js';
import type { Node, Edge } from '../../rendering-util/types.js';
import type { KanbanInternalNode } from './kanbanTypes.js';
import type { Node, 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 nodes: KanbanInternalNode[] = [];
let sections: KanbanInternalNode[] = [];
let cnt = 0;
let elements: Record<number, D3Element> = {};
@ -38,9 +40,6 @@ const getSection = function (level: number) {
throw new Error('Items without section detected, found section ("' + nodes[i].descr + '")');
}
}
// if (!lastSection) {
// // console.log('No last section');
// }
if (level === lastSection?.level) {
return null;
}
@ -59,15 +58,15 @@ const getData = function () {
const sections = getSections();
const conf = getConfig();
// const id: string = sanitizeText(id, conf) || 'identifier' + cnt++;
for (const section of sections) {
const node = {
id: section.nodeId,
label: sanitizeText(section.descr, conf),
isGroup: true,
ticket: section.ticket,
shape: 'kanbanSection',
} satisfies Node;
} satisfies KanbanNode;
nodes.push(node);
for (const item of section.children) {
const childNode = {
@ -75,10 +74,14 @@ const getData = function () {
parentId: section.nodeId,
label: sanitizeText(item.descr, conf),
isGroup: false,
ticket: item?.ticket,
priority: item?.priority,
assigned: item?.assigned,
icon: item?.icon,
shape: 'kanbanItem',
rx: 5,
cssStyles: ['text-align: left'],
} satisfies Node;
} satisfies KanbanNode;
nodes.push(childNode);
}
}
@ -86,7 +89,7 @@ const getData = function () {
return { nodes, edges, other: {}, config: getConfig() };
};
const addNode = (level: number, id: string, descr: string, type: number) => {
const addNode = (level: number, id: string, descr: string, type: number, shapeData: string) => {
// log.info('addNode level=', level, 'id=', id, 'descr=', descr, 'type=', type);
const conf = getConfig();
let padding: number = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
@ -106,9 +109,47 @@ const addNode = (level: number, id: string, descr: string, type: number) => {
children: [],
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
padding,
} satisfies KanbanNode;
} satisfies KanbanInternalNode;
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.type = doc?.shape;
}
if (doc?.label) {
node.descr = 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);
console.log('Node ', node.descr, ' section', section?.descr);
if (section) {
section.children.push(node);
// Keep all nodes in the list

View File

@ -1,86 +1,13 @@
import type cytoscape from 'cytoscape';
// @ts-expect-error No types available
import coseBilkent from 'cytoscape-cose-bilkent';
import { select } from 'd3';
import type { MermaidConfig } from '../../config.type.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { DrawDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import type { D3Element } from '../../types.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import type { KanbanDB, KanbanNode } from './kanbanTypes.js';
import type { KanbanDB } from './kanbanTypes.js';
import defaultConfig from '../../defaultConfig.js';
import { insertCluster, positionCluster } from '../../rendering-util/rendering-elements/clusters';
import { insertNode, positionNode } from '../../rendering-util/rendering-elements/nodes';
declare module 'cytoscape' {
interface EdgeSingular {
_private: {
bodyBounds: unknown;
rscratch: {
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
};
};
}
}
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
cy.edges().map((edge, id) => {
const data = edge.data();
if (edge[0]._private.bodyBounds) {
const bounds = edge[0]._private.rscratch;
log.trace('Edge: ', id, data);
edgesEl
.insert('path')
.attr(
'd',
`M ${bounds.startX},${bounds.startY} L ${bounds.midX},${bounds.midY} L${bounds.endX},${bounds.endY} `
)
.attr('class', 'edge section-edge-' + data.section + ' edge-depth-' + data.depth);
}
});
}
function addNodes(mindmap: KanbanNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
cy.add({
group: 'nodes',
data: {
id: mindmap.id.toString(),
labelText: mindmap.descr,
height: mindmap.height,
width: mindmap.width,
level: level,
nodeId: mindmap.id,
padding: mindmap.padding,
type: mindmap.type,
},
position: {
x: mindmap.x!,
y: mindmap.y!,
},
});
if (mindmap.children) {
mindmap.children.forEach((child) => {
addNodes(child, cy, conf, level + 1);
cy.add({
group: 'edges',
data: {
id: `${mindmap.id}_${child.id}`,
source: mindmap.id,
target: child.id,
depth: level,
section: child.section,
},
});
});
}
}
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 mindmap diagram\n' + text);
@ -106,8 +33,9 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
const padding = 10;
for (const section of sections) {
const WIDTH = 200;
let y = (-WIDTH * 3) / 2 + 40;
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;
@ -118,21 +46,21 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
// Todo, use theme variable THEME_COLOR_LIMIT instead of 10
section.cssClasses = section.cssClasses + ' section-' + cnt;
const cluster = await insertCluster(sectionsElem, section);
const sectionObj = await insertCluster(sectionsElem, section);
const sectionItems = data4Layout.nodes.filter((node) => node.parentId === section.id);
// positionCluster(section);
for (const item of sectionItems) {
item.x = section.x;
item.width = WIDTH - 2 * padding;
// item.height = 100;
const nodeEl = await insertNode(nodesElem, item);
console.log('ITEM', item, 'bbox=', nodeEl.node().getBBox());
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;
// item.height = 150;
await positionNode(item);
y = item.y + bbox.height / 2 + padding;
y = item.y + bbox.height / 2 + padding / 2;
}
const rect = sectionObj.cluster.select('rect');
const height = Math.max(y - top + 3 * padding, 50);
rect.attr('height', height);
}
// Setup the view box and size of the svg element

View File

@ -1,22 +1,24 @@
import type { RequiredDeep } from 'type-fest';
import type kanbanDb from './kanbanDb.js';
export interface KanbanNode {
export interface KanbanInternalNode {
id: number;
nodeId: string;
level: number;
descr: string;
type: number;
children: KanbanNode[];
children: KanbanInternalNode[];
width: number;
padding: number;
section?: number;
height?: number;
class?: string;
icon?: string;
ticket?: string;
priority?: string;
x?: number;
y?: number;
}
export type FilledKanbanNode = RequiredDeep<KanbanNode>;
export type FilledKanbanNode = RequiredDeep<KanbanInternalNode>;
export type KanbanDB = typeof kanbanDb;

View File

@ -15,12 +15,39 @@
%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';
"kanban" {return 'KANBAN';}
":::" { this.begin('CLASS'); }
<CLASS>.+ { this.popState();return 'CLASS'; }
<CLASS>\n { this.popState();}
@ -40,7 +67,7 @@
"[" { this.begin('NODE');return 'NODE_DSTART'; }
[\s]+ return 'SPACELIST' /* skip all whitespace */ ;
// !(-\() return 'NODE_ID';
[^\(\[\n\)\{\}]+ return 'NODE_ID';
[^\(\[\n\)\{\}@]+ {return 'NODE_ID';}
<<EOF>> return 'EOF';
<NODE>["][`] { this.begin("NSTR2");}
<NSTR2>[^`"]+ { return "NODE_DESCR";}
@ -97,10 +124,12 @@ document
;
statement
: SPACELIST node { yy.getLogger().info('Node: ',$2.id);yy.addNode($1.length, $2.id, $2.descr, $2.type); }
: 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}); }
@ -120,8 +149,18 @@ nodeWithoutId
;
nodeWithId
: NODE_ID { $$ = { id: $1, descr: $1, type: yy.nodeType.DEFAULT }; }
: 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

@ -14,13 +14,16 @@ const genSections: DiagramStylesProvider = (options) => {
}
}
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: ${options['cScale' + i]};
fill: ${adjuster(options['cScale' + i], 10)};
}
.section-${i - 1} text {
fill: ${options['cScaleLabel' + i]};
@ -56,6 +59,12 @@ const genSections: DiagramStylesProvider = (options) => {
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
.kanban-ticket-link {
fill: ${options.background};
stroke: ${options.nodeBorder};
text-decoration: underline;
}
`;
}
return sections;

View File

@ -1,29 +1,97 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import { labelHelper, insertLabel, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import type { KanbanNode } from '../../types.js';
import { createRoundedRectPathD } from './roundedRectPath.js';
import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const kanbanItem = async (parent: SVGAElement, node: Node) => {
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: SVGAElement, node: KanbanNode, { config }) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
// console.log('IPI labelStyles:', labelStyles);
// const labelPaddingX = 10;
const labelPaddingX = 10;
const orgWidth = node.width;
node.width = (node.width ?? 200) - 2 * labelPaddingX;
console.log('APA123 kanbanItem', node.labelStyle);
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
node.width = (node.width ?? 200) - 10;
// console.log('APA123 kanbanItem priority', node?.priority);
const {
shapeSvg,
bbox,
label: labelElTitle,
} = await labelHelper(parent, node, getNodeClasses(node));
const padding = node.padding || 10;
// elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
// console.log('STO node config.kanban:', config.kanban, config.kanban);
let ticketUrl = '';
let link;
// console.log('STO ticket:', node.ticket);
if (node.ticket && config.kanban.ticketBaseUrl) {
ticketUrl = config.kanban.ticketBaseUrl.replace('#TICKET#', node.ticket);
link = shapeSvg
.insert('svg:a', ':first-child')
.attr('class', 'kanban-ticket-link')
.attr('xlink:href', ticketUrl)
.attr('target', '_blank');
}
const options = {
useHtmlLabels: node.useHtmlLabels,
labelStyle: node.labelStyle,
width: node.width,
icon: node.icon,
img: node.img,
padding: node.padding,
centerLabel: false,
};
const { label: labelEl, bbox: bbox2 } = await insertLabel(
link ? link : shapeSvg,
node.ticket || '',
options
);
const { label: labelElAssigned, bbox: bboxAssigned } = await insertLabel(
shapeSvg,
node.assigned || '',
options
);
node.width = orgWidth;
const labelPaddingY = 10;
const totalWidth = node?.width || 0;
const totalHeight = Math.max(bbox.height + labelPaddingY * 2, node?.height || 0);
const heightAdj = Math.max(bbox2.height, bboxAssigned.height) / 2;
const totalHeight = Math.max(bbox.height + labelPaddingY * 2, node?.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) +
')'
);
// log.info('IPI node = ', node);
let rect;
const { rx, ry } = node;
const { cssStyles } = node;
@ -51,9 +119,25 @@ export const kanbanItem = async (parent: SVGAElement, node: Node) => {
.attr('y', y)
.attr('width', totalWidth)
.attr('height', totalHeight);
if (node.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(node.priority));
}
}
updateNodeBounds(node, rect);
node.height = totalHeight;
node.intersect = function (point) {
return intersect.rect(node, point);

View File

@ -4,7 +4,7 @@ import { select } from 'd3';
import { evaluate, sanitizeText } from '../../../diagrams/common/common.js';
import { decodeEntities } from '../../../utils.js';
export const labelHelper = async (parent, node, _classes) => {
export const labelHelper = async (parent, node, _classes, _shapeSvg) => {
let cssClasses;
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig().flowchart.htmlLabels);
if (!_classes) {
@ -14,10 +14,12 @@ export const labelHelper = async (parent, node, _classes) => {
}
// 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.insert('g').attr('class', 'label').attr('style', node.labelStyle);
@ -106,6 +108,46 @@ export const labelHelper = async (parent, node, _classes) => {
return { shapeSvg, bbox, halfPadding, label: labelEl };
};
export const insertLabel = async (parent, label, options) => {
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);
let text;
text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {
useHtmlLabels,
width: options.width || getConfig().flowchart.wrappingWidth,
cssClasses: 'markdown-node-label',
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 = (node, element) => {
const bbox = element.node().getBBox();
node.width = bbox.width;

View File

@ -148,3 +148,11 @@ export interface ShapeRenderOptions {
config: MermaidConfig;
dir: string;
}
export interface KanbanNode extends Node {
// Kanban specif data
priority?: 'Very High' | 'High' | 'Medium' | 'Low' | 'Very Low';
ticket?: string;
assigned?: string;
icon?: string;
}

View File

@ -279,6 +279,8 @@ properties:
$ref: '#/$defs/ArchitectureDiagramConfig'
mindmap:
$ref: '#/$defs/MindmapDiagramConfig'
kanban:
$ref: '#/$defs/KanbanDiagramConfig'
gitGraph:
$ref: '#/$defs/GitGraphDiagramConfig'
c4:
@ -964,6 +966,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,7 @@ export interface NodeMetaData {
w?: string;
h?: string;
constraint?: 'on' | 'off';
priority: 'Very High' | 'High' | 'Medium' | 'Low' | 'Very Low';
}
import type { MermaidConfig } from './config.type.js';