mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
Adding ticket handling
This commit is contained in:
parent
93f2c241b8
commit
290c678dc7
@ -19,6 +19,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
||||
'xyChart',
|
||||
'requirement',
|
||||
'mindmap',
|
||||
'kanban',
|
||||
'timeline',
|
||||
'gitGraph',
|
||||
'c4',
|
||||
|
@ -12,6 +12,7 @@ gantt
|
||||
gitgraph
|
||||
gzipped
|
||||
handDrawn
|
||||
kanban
|
||||
knsv
|
||||
Knut
|
||||
marginx
|
||||
|
@ -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">
|
||||
|
@ -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".
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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; }
|
||||
;
|
||||
|
||||
|
||||
|
||||
%%
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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' }]
|
||||
|
@ -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';
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user