mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-28 07:03:17 +08:00
#3358 Adding support for column statements
This commit is contained in:
parent
da79b371fe
commit
a641fd51e8
@ -65,27 +65,62 @@
|
||||
<body>
|
||||
<pre id="diagram" class="mermaid">
|
||||
block-beta
|
||||
id1("Wide 1")
|
||||
%%id2("2")
|
||||
block
|
||||
id3["I am a wide one"]
|
||||
block
|
||||
id44("A final one")
|
||||
id45("B final one")
|
||||
end
|
||||
columns 1
|
||||
id1
|
||||
id2
|
||||
id3("Wider then")
|
||||
end
|
||||
id4("Another final one")
|
||||
|
||||
id4
|
||||
</pre>
|
||||
|
||||
<pre id="diagram" class="mermaid">
|
||||
<pre id="diagram" class="mermaid2">
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
block
|
||||
columns 3
|
||||
id1
|
||||
id2
|
||||
id2.1
|
||||
%%id2.2
|
||||
end
|
||||
id48
|
||||
end
|
||||
id3
|
||||
%% id3
|
||||
%% id4
|
||||
%% block
|
||||
%% columns 2
|
||||
%% id2
|
||||
%% id3
|
||||
%% end
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
block-beta
|
||||
block
|
||||
columns 1
|
||||
id1
|
||||
id2
|
||||
%%id2.1
|
||||
end
|
||||
id3
|
||||
%% id3
|
||||
%% id4
|
||||
%% block
|
||||
%% columns 2
|
||||
%% id2
|
||||
%% id3
|
||||
%% end
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid2">
|
||||
block-beta
|
||||
id1
|
||||
block
|
||||
id2
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
<pre id="diagram" class="mermaid2">
|
||||
block-beta
|
||||
id1["Hello"]
|
||||
block
|
||||
@ -96,7 +131,7 @@ block-beta
|
||||
id5["World"]
|
||||
end
|
||||
</pre>
|
||||
<pre id="diagram" class="mermaid">
|
||||
<pre id="diagram" class="mermaid2">
|
||||
block-beta
|
||||
columns 2
|
||||
block
|
||||
|
@ -1,9 +1,8 @@
|
||||
// import type { BlockDB } from './blockTypes.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { BlockConfig, BlockType, Block, Link } from './blockTypes.js';
|
||||
import type { BlockConfig, BlockType, Block, Link } from './blockTypes.js';
|
||||
|
||||
import * as configApi from '../../config.js';
|
||||
// import common from '../common/common.js';
|
||||
import {
|
||||
// setAccTitle,
|
||||
// getAccTitle,
|
||||
@ -37,9 +36,8 @@ const populateBlockDatabase = (blockList: Block[], parent: Block): void => {
|
||||
if (block.children) {
|
||||
populateBlockDatabase(block.children, block);
|
||||
}
|
||||
if (block.type !== 'column-setting') {
|
||||
children.push(block);
|
||||
}
|
||||
|
||||
children.push(block);
|
||||
}
|
||||
}
|
||||
parent.children = children;
|
||||
@ -79,9 +77,10 @@ export const generateId = () => {
|
||||
|
||||
type ISetHierarchy = (block: Block[]) => void;
|
||||
const setHierarchy = (block: Block[]): void => {
|
||||
rootBlock.children = block;
|
||||
populateBlockDatabase(block, rootBlock);
|
||||
log.debug('The hierarchy', JSON.stringify(block, null, 2));
|
||||
blocks = block;
|
||||
log.debug('The hierarchy', JSON.stringify(rootBlock, null, 2));
|
||||
blocks = rootBlock.children;
|
||||
};
|
||||
|
||||
type IAddLink = (link: Link) => Link;
|
||||
|
@ -7,16 +7,14 @@ import {
|
||||
select as d3select,
|
||||
scaleOrdinal as d3scaleOrdinal,
|
||||
schemeTableau10 as d3schemeTableau10,
|
||||
ContainerElement,
|
||||
} from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
|
||||
import { BlockDB } from './blockDB.js';
|
||||
import type { Block } from './blockTypes.js';
|
||||
|
||||
// import { diagram as BlockDiagram } from './blockDiagram.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import { Uid } from '../../rendering-util/uid.js';
|
||||
import { pad } from 'lodash';
|
||||
|
||||
export const draw = async function (
|
||||
text: string,
|
||||
@ -43,27 +41,28 @@ export const draw = async function (
|
||||
const nodes = svg.insert('g').attr('class', 'block');
|
||||
await calculateBlockSizes(nodes, bl, db);
|
||||
const bounds = layout(db);
|
||||
console.log('Here blocks', bl);
|
||||
log.debug('Here blocks', bl);
|
||||
await insertBlocks(nodes, bl, db);
|
||||
|
||||
// console.log('Here', bl);
|
||||
// log.debug('Here', bl);
|
||||
|
||||
// Establish svg dimensions and get width and height
|
||||
//
|
||||
// const bounds2 = nodes.node().getBoundingClientRect();
|
||||
const bounds2 = bounds;
|
||||
const padding = 10;
|
||||
// Why, oh why ????
|
||||
const magicFactor = Math.max(1, Math.round(0.125 * (bounds2.width / bounds2.height)));
|
||||
const height = bounds2.height + magicFactor + 10;
|
||||
const width = bounds2.width + 10;
|
||||
const useMaxWidth = false;
|
||||
configureSvgSize(svg, height, width, useMaxWidth);
|
||||
console.log('Here Bounds', bounds, bounds2);
|
||||
svg.attr(
|
||||
'viewBox',
|
||||
`${bounds2.x - 5} ${bounds2.y - 5} ${bounds2.width + 10} ${bounds2.height + 10}`
|
||||
);
|
||||
if (bounds) {
|
||||
const bounds2 = bounds;
|
||||
const magicFactor = Math.max(1, Math.round(0.125 * (bounds2.width / bounds2.height)));
|
||||
const height = bounds2.height + magicFactor + 10;
|
||||
const width = bounds2.width + 10;
|
||||
const useMaxWidth = false;
|
||||
configureSvgSize(svg, height, width, useMaxWidth);
|
||||
log.debug('Here Bounds', bounds, bounds2);
|
||||
svg.attr(
|
||||
'viewBox',
|
||||
`${bounds2.x - 5} ${bounds2.y - 5} ${bounds2.width + 10} ${bounds2.height + 10}`
|
||||
);
|
||||
}
|
||||
// svg.attr('viewBox', `${-200} ${-200} ${400} ${400}`);
|
||||
|
||||
// Prepare data for construction based on diagObj.db
|
||||
@ -83,92 +82,6 @@ export const draw = async function (
|
||||
y?: number;
|
||||
}
|
||||
|
||||
const blocks: LayedBlock[] = [
|
||||
{
|
||||
ID: 'ApplicationLayer',
|
||||
label: 'Application Layer',
|
||||
x: 0,
|
||||
y: 0,
|
||||
children: [
|
||||
{
|
||||
ID: 'UserInterface',
|
||||
label: 'User Interface (WPF, HTML5/CSS3, Swing)',
|
||||
x: 0,
|
||||
y: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ID: 'PresentationLayer',
|
||||
label: 'Presentation Layer',
|
||||
x: 0,
|
||||
y: 50,
|
||||
children: [
|
||||
{
|
||||
ID: 'Smack',
|
||||
label: 'J2SE Mobil App (Smack)',
|
||||
},
|
||||
{
|
||||
ID: 'JsJAC',
|
||||
label: 'Java Script Browser App (JsJAC)',
|
||||
},
|
||||
{
|
||||
ID: 'babelim',
|
||||
label: '.NET Windows App (Babel-im)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ID: 'SessionLayer',
|
||||
label: 'Session Layer',
|
||||
x: 0,
|
||||
y: 100,
|
||||
children: [
|
||||
{
|
||||
ID: 'XMPP',
|
||||
label: 'XMPP Component',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
ID: 'Authentication',
|
||||
label: 'Authentication',
|
||||
},
|
||||
{
|
||||
ID: 'Authorization',
|
||||
label: 'Authorization',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ID: 'LDAP',
|
||||
label: 'LDAP, DB, POP',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ID: 'NetworkLayer',
|
||||
label: 'Network Layer',
|
||||
x: 0,
|
||||
y: 150,
|
||||
children: [
|
||||
{ ID: 'HTTP', label: 'HTTP' },
|
||||
{ ID: 'SOCK', label: 'SOCK' },
|
||||
],
|
||||
},
|
||||
{
|
||||
ID: 'DataLayer',
|
||||
label: 'Data Layer',
|
||||
x: 0,
|
||||
y: 200,
|
||||
children: [
|
||||
{ ID: 'XMPP', label: 'XMPP' },
|
||||
{ ID: 'BDB', label: 'Business DB' },
|
||||
{ ID: 'AD', label: 'Active Directory' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Get color scheme for the graph
|
||||
const colorScheme = d3scaleOrdinal(d3schemeTableau10);
|
||||
};
|
||||
|
@ -39,6 +39,7 @@ export interface Block {
|
||||
};
|
||||
node?: any;
|
||||
columns?: number; // | TBlockColumnsDefaultValue;
|
||||
classes?: string[];
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
|
13
packages/mermaid/src/diagrams/block/layout.spec.ts
Normal file
13
packages/mermaid/src/diagrams/block/layout.spec.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// @ts-ignore: jison doesn't export types
|
||||
import { calculateBlockPosition } from './layout.js';
|
||||
|
||||
describe('Layout', function () {
|
||||
it('It shoud calulatepositions correctly', () => {
|
||||
expect(calculateBlockPosition(2, 0)).toEqual({ px: 0, py: 0 });
|
||||
expect(calculateBlockPosition(2, 1)).toEqual({ px: 1, py: 0 });
|
||||
expect(calculateBlockPosition(2, 2)).toEqual({ px: 0, py: 1 });
|
||||
expect(calculateBlockPosition(2, 3)).toEqual({ px: 1, py: 1 });
|
||||
expect(calculateBlockPosition(2, 4)).toEqual({ px: 0, py: 2 });
|
||||
expect(calculateBlockPosition(1, 3)).toEqual({ px: 0, py: 2 });
|
||||
});
|
||||
});
|
@ -1,10 +1,41 @@
|
||||
import { BlockDB } from './blockDB.js';
|
||||
import type { Block } from './blockTypes.js';
|
||||
import { log } from '../../logger.js';
|
||||
const padding = 8;
|
||||
|
||||
const padding = 10;
|
||||
interface BlockPosition {
|
||||
px: number;
|
||||
py: number;
|
||||
}
|
||||
|
||||
export function calculateBlockPosition(columns: number, position: number): BlockPosition {
|
||||
// Ensure that columns is a positive integer
|
||||
if (columns === 0 || !Number.isInteger(columns)) {
|
||||
throw new Error('Columns must be an integer !== 0.');
|
||||
}
|
||||
|
||||
// Ensure that position is a non-negative integer
|
||||
if (position < 0 || !Number.isInteger(position)) {
|
||||
throw new Error('Position must be a non-negative integer.');
|
||||
}
|
||||
|
||||
if (columns < 0) {
|
||||
// Auto coulumns is set
|
||||
return { px: position, py: 0 };
|
||||
}
|
||||
if (columns === 1) {
|
||||
// Auto coulumns is set
|
||||
return { px: 0, py: position };
|
||||
}
|
||||
// Calculate posX and posY
|
||||
const px = position % columns;
|
||||
const py = Math.floor(position / columns);
|
||||
|
||||
return { px, py };
|
||||
}
|
||||
|
||||
function calcBlockSizes(block: Block, db: BlockDB) {
|
||||
console.log('calculateSize (start)', block.id, block?.size?.x, block?.size?.width);
|
||||
log.debug('calculateSize (start)', block.id, block?.size?.x, block?.size?.width);
|
||||
const totalWidth = 0;
|
||||
const totalHeight = 0;
|
||||
let maxWidth = 0;
|
||||
@ -17,7 +48,7 @@ function calcBlockSizes(block: Block, db: BlockDB) {
|
||||
// find max width of children
|
||||
for (const child of block.children) {
|
||||
const { width, height, x, y } = child.size || { width: 0, height: 0, x: 0, y: 0 };
|
||||
// console.log('APA', child.id, width, height, x, y);
|
||||
// log.debug('APA', child.id, width, height, x, y);
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width;
|
||||
}
|
||||
@ -51,105 +82,133 @@ function calcBlockSizes(block: Block, db: BlockDB) {
|
||||
// }
|
||||
}
|
||||
if (block.children?.length > 0) {
|
||||
const columns = block.columns || -1;
|
||||
const numItems = block.children.length;
|
||||
|
||||
// The width and height in number blocks
|
||||
let xSize = block.children?.length;
|
||||
if (columns > 0 && columns < numItems) {
|
||||
xSize = columns;
|
||||
}
|
||||
const ySize = Math.ceil(numItems / xSize);
|
||||
|
||||
log.debug(
|
||||
'(calc)',
|
||||
block.id,
|
||||
'xSize',
|
||||
xSize,
|
||||
'ySize',
|
||||
ySize,
|
||||
'columns',
|
||||
columns,
|
||||
block.children.length
|
||||
);
|
||||
|
||||
const numChildren = block.children.length;
|
||||
block.size = {
|
||||
width: numChildren * (maxWidth + padding) + padding,
|
||||
height: maxHeight + 2 * padding,
|
||||
// width: numChildren * (maxWidth + padding) + padding,
|
||||
width: xSize * (maxWidth + padding) + padding,
|
||||
// height: maxHeight + 2 * padding,
|
||||
height: ySize * (maxHeight + padding) + padding,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
console.log('calculateSize APA (done)', block.id, block.size.x, block.size.width);
|
||||
log.debug('calculateSize APA (done)', block.id, block?.size?.x, block?.size?.width);
|
||||
}
|
||||
|
||||
function layoutBlocks(block: Block, db: BlockDB) {
|
||||
console.log('layout blocks (block)', block.id, 'x:', block.size.x, 'width:', block.size.width);
|
||||
log.debug(
|
||||
'layout blocks (=>layoutBlocks)',
|
||||
block.id,
|
||||
'x:',
|
||||
block?.size?.x,
|
||||
'width:',
|
||||
block?.size?.width
|
||||
);
|
||||
const columns = block.columns || -1;
|
||||
log.debug('layoutBlocks columns', block.id, '=>', columns);
|
||||
if (
|
||||
block.children && // find max width of children
|
||||
block.children.length > 0
|
||||
) {
|
||||
const width = block?.children[0]?.size?.width || 0;
|
||||
const widthOfChildren = block.children.length * width + (block.children.length - 1) * padding;
|
||||
let posX = (block?.size?.x || 0) - widthOfChildren / 2;
|
||||
const posY = 0;
|
||||
const parentX = block?.size?.x || 0 - block.children.length;
|
||||
const parentWidth = block?.size?.width || 0;
|
||||
|
||||
console.log('widthOfChildren', widthOfChildren, 'posX', posX, 'parentX', parentX);
|
||||
log.debug('widthOfChildren', widthOfChildren, 'posX');
|
||||
|
||||
// let first = true;
|
||||
let columnPos = -1;
|
||||
for (const child of block.children) {
|
||||
console.log(
|
||||
'layout blocks (child)',
|
||||
child.id,
|
||||
'x:',
|
||||
child?.size?.x,
|
||||
'width:',
|
||||
child?.size?.width,
|
||||
'posX:',
|
||||
posX,
|
||||
block?.size?.x,
|
||||
widthOfChildren / 2,
|
||||
widthOfChildren / 2
|
||||
);
|
||||
columnPos++;
|
||||
|
||||
// log.debug(
|
||||
// 'layout blocks (child)',
|
||||
// child.id,
|
||||
// 'x:',
|
||||
// child?.size?.x,
|
||||
// 'width:',
|
||||
// child?.size?.width,
|
||||
// 'posX:',
|
||||
// posX,
|
||||
// block?.size?.x,
|
||||
// widthOfChildren / 2,
|
||||
// widthOfChildren / 2
|
||||
// );
|
||||
|
||||
if (!child.size) {
|
||||
continue;
|
||||
}
|
||||
const { width, height } = child.size;
|
||||
child.size.x = posX + width / 2;
|
||||
posX += width + padding;
|
||||
child.size.y = posY;
|
||||
const { px, py } = calculateBlockPosition(columns, columnPos);
|
||||
log.debug(
|
||||
'layout blocks (child) px, py (',
|
||||
block?.size?.x,
|
||||
',',
|
||||
block?.size?.y,
|
||||
')',
|
||||
'parent:',
|
||||
block.id,
|
||||
width / 2,
|
||||
padding
|
||||
);
|
||||
if (block.size) {
|
||||
child.size.x =
|
||||
block.size.x - block.size.width / 2 + px * (width + padding) + width / 2 + padding;
|
||||
// child.size.x = px * (width + padding) - block.size.width / 2;
|
||||
// posX += width + padding;
|
||||
// child.size.y = py * (height + padding) + height / 2 + padding;
|
||||
child.size.y =
|
||||
block.size.y - block.size.height / 2 + py * (height + padding) + height / 2 + padding;
|
||||
|
||||
log.debug(
|
||||
'layout blocks (calc) px, py',
|
||||
'id:',
|
||||
child.id,
|
||||
'=>',
|
||||
'x:',
|
||||
child.size.x,
|
||||
'y:',
|
||||
child.size.y
|
||||
);
|
||||
}
|
||||
|
||||
// posY += height + padding;
|
||||
if (child.children) {
|
||||
layoutBlocks(child, db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function positionBlock(parent: Block, block: Block, db: BlockDB) {
|
||||
console.log(
|
||||
'layout position block',
|
||||
parent.id,
|
||||
parent?.size?.x,
|
||||
log.debug(
|
||||
'layout blocks (<==layoutBlocks)',
|
||||
block.id,
|
||||
'x:',
|
||||
block?.size?.x,
|
||||
'width:',
|
||||
block?.size?.width
|
||||
);
|
||||
let parentX = 0;
|
||||
let parentWidth = 0;
|
||||
let y = 0;
|
||||
if (parent.id !== 'root') {
|
||||
parentX = parent?.size?.x || 0;
|
||||
parentWidth = parent?.size?.width || 0;
|
||||
y = parent?.size?.y || 0;
|
||||
}
|
||||
if (block.size && block.id !== 'root') {
|
||||
console.log(
|
||||
'layout position block (calc)',
|
||||
'x:',
|
||||
parentX,
|
||||
parentWidth / 2,
|
||||
block.id,
|
||||
'x:',
|
||||
block.size.x,
|
||||
block.size.width
|
||||
);
|
||||
// block.size.x = parentX + block.size.x + -block.size.width / 2;
|
||||
block.size.x =
|
||||
parentX < 0 ? parentX + block.size.x : parentX + block.size.x + -block.size.width / 2;
|
||||
// block.size.x = parentX - parentWidth + Math.abs(block.size.x) / 2;
|
||||
block.size.y = block.size.y + y;
|
||||
}
|
||||
if (block.children) {
|
||||
for (const child of block.children) {
|
||||
positionBlock(block, child, db);
|
||||
}
|
||||
}
|
||||
// console.log('layout position block', block);
|
||||
}
|
||||
|
||||
let minX = 0;
|
||||
let minY = 0;
|
||||
let maxX = 0;
|
||||
@ -160,7 +219,7 @@ function findBounds(block: Block) {
|
||||
const { x, y, width, height } = block.size;
|
||||
if (x - width / 2 < minX) {
|
||||
minX = x - width / 2;
|
||||
// console.log('Here APA minX', block.id, x, width, minX);
|
||||
// log.debug('Here APA minX', block.id, x, width, minX);
|
||||
}
|
||||
if (y - height / 2 < minY) {
|
||||
minY = y - height / 2;
|
||||
@ -180,20 +239,22 @@ function findBounds(block: Block) {
|
||||
}
|
||||
|
||||
export function layout(db: BlockDB) {
|
||||
const blocks = db.getBlocks();
|
||||
const root = { id: 'root', type: 'composite', children: blocks } as Block;
|
||||
const root = db.getBlock('root');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
calcBlockSizes(root, db);
|
||||
layoutBlocks(root, db);
|
||||
// Position blocks relative to parents
|
||||
// positionBlock(root, root, db);
|
||||
console.log('getBlocks', JSON.stringify(db.getBlocks(), null, 2));
|
||||
log.debug('getBlocks', JSON.stringify(root, null, 2));
|
||||
|
||||
minX = 0;
|
||||
minY = 0;
|
||||
maxX = 0;
|
||||
maxY = 0;
|
||||
findBounds(root);
|
||||
// console.log('Here maxX', minX, '--', maxX);
|
||||
// log.debug('Here maxX', minX, '--', maxX);
|
||||
const height = maxY - minY;
|
||||
const width = maxX - minX;
|
||||
return { x: minX, y: minY, width, height };
|
||||
|
@ -5,28 +5,23 @@ import { ContainerElement } from 'd3';
|
||||
import type { Block } from './blockTypes.js';
|
||||
import { BlockDB } from './blockDB.js';
|
||||
|
||||
interface Node {
|
||||
classes: string;
|
||||
}
|
||||
|
||||
function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) {
|
||||
const vertex = block;
|
||||
|
||||
/**
|
||||
* Variable for storing the classes for the vertex
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
let classStr = 'default';
|
||||
if ((vertex?.classes?.length || []) > 0) {
|
||||
classStr = vertex.classes.join(' ');
|
||||
if ((vertex?.classes?.length || 0) > 0) {
|
||||
classStr = (vertex?.classes || []).join(' ');
|
||||
}
|
||||
classStr = classStr + ' flowchart-label';
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
let vertexNode;
|
||||
const labelData = { width: 0, height: 0 };
|
||||
|
||||
let radious = 0;
|
||||
let _shape = '';
|
||||
let layoutOptions = {};
|
||||
console.log('This is the type:', vertex.type);
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
@ -140,20 +135,18 @@ async function calculateBlockSize(elem: any, block: any, db: any) {
|
||||
const boundingBox = nodeEl.node().getBBox();
|
||||
const obj = db.getBlock(node.id);
|
||||
obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl };
|
||||
console.log('Here boundsíng', boundingBox.width);
|
||||
db.setBlock(obj);
|
||||
nodeEl.remove();
|
||||
}
|
||||
|
||||
export async function insertBlockPositioned(elem: any, block: any, db: any) {
|
||||
console.log('Here insertBlockPositioned');
|
||||
const node = getNodeFromBlock(block, db, true);
|
||||
// if (node.type === 'composite') {
|
||||
// return;
|
||||
// }
|
||||
// Add the element to the DOM to size it
|
||||
const obj = db.getBlock(node.id);
|
||||
const nodeEl = await insertNode(elem, node);
|
||||
// const obj = db.getBlock(node.id);
|
||||
// const nodeEl = await insertNode(elem, node);
|
||||
positionNode(node);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user