mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Refactor: e -> event or error, err -> error, p -> pointer (#831)
* Refactor: e -> event or error, err -> error, p -> pointer * simplify
This commit is contained in:
parent
d2827aa40b
commit
097c5dfad7
14 changed files with 190 additions and 185 deletions
202
src/index.tsx
202
src/index.tsx
|
@ -151,7 +151,7 @@ document.addEventListener(
|
|||
{ passive: false },
|
||||
);
|
||||
|
||||
let lastPointerUp: ((e: any) => void) | null = null;
|
||||
let lastPointerUp: ((event: any) => void) | null = null;
|
||||
const gesture: Gesture = {
|
||||
pointers: [],
|
||||
lastCenter: null,
|
||||
|
@ -712,22 +712,22 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onCut = (e: ClipboardEvent) => {
|
||||
if (isWritableElement(e.target)) {
|
||||
private onCut = (event: ClipboardEvent) => {
|
||||
if (isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
copyToAppClipboard(elements);
|
||||
elements = deleteSelectedElements(elements);
|
||||
history.resumeRecording();
|
||||
this.setState({});
|
||||
e.preventDefault();
|
||||
event.preventDefault();
|
||||
};
|
||||
private onCopy = (e: ClipboardEvent) => {
|
||||
if (isWritableElement(e.target)) {
|
||||
private onCopy = (event: ClipboardEvent) => {
|
||||
if (isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
copyToAppClipboard(elements);
|
||||
e.preventDefault();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
private onUnload = () => {
|
||||
|
@ -736,8 +736,8 @@ export class App extends React.Component<any, AppState> {
|
|||
this.saveDebounced.flush();
|
||||
};
|
||||
|
||||
private disableEvent: EventHandlerNonNull = e => {
|
||||
e.preventDefault();
|
||||
private disableEvent: EventHandlerNonNull = event => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
private unmounted = false;
|
||||
|
@ -803,9 +803,9 @@ export class App extends React.Component<any, AppState> {
|
|||
this.setState({});
|
||||
};
|
||||
|
||||
private updateCurrentCursorPosition = (e: MouseEvent) => {
|
||||
cursorX = e.x;
|
||||
cursorY = e.y;
|
||||
private updateCurrentCursorPosition = (event: MouseEvent) => {
|
||||
cursorX = event.x;
|
||||
cursorY = event.y;
|
||||
};
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent) => {
|
||||
|
@ -907,18 +907,18 @@ export class App extends React.Component<any, AppState> {
|
|||
copyToAppClipboard(elements);
|
||||
};
|
||||
|
||||
private pasteFromClipboard = async (e: ClipboardEvent | null) => {
|
||||
private pasteFromClipboard = async (event: ClipboardEvent | null) => {
|
||||
// #686
|
||||
const target = document.activeElement;
|
||||
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
||||
if (
|
||||
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
||||
// thus these checks don't make sense
|
||||
!e ||
|
||||
!event ||
|
||||
(elementUnderCursor instanceof HTMLCanvasElement &&
|
||||
!isWritableElement(target))
|
||||
) {
|
||||
const data = await getClipboardContent(e);
|
||||
const data = await getClipboardContent(event);
|
||||
if (data.elements) {
|
||||
this.addElementsFromPaste(data.elements);
|
||||
} else if (data.text) {
|
||||
|
@ -950,7 +950,7 @@ export class App extends React.Component<any, AppState> {
|
|||
history.resumeRecording();
|
||||
}
|
||||
this.selectShapeTool("selection");
|
||||
e?.preventDefault();
|
||||
event?.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -976,8 +976,10 @@ export class App extends React.Component<any, AppState> {
|
|||
this.setState({});
|
||||
};
|
||||
|
||||
removePointer = (e: React.PointerEvent<HTMLElement>) => {
|
||||
gesture.pointers = gesture.pointers.filter(p => p.id !== e.pointerId);
|
||||
removePointer = (event: React.PointerEvent<HTMLElement>) => {
|
||||
gesture.pointers = gesture.pointers.filter(
|
||||
pointer => pointer.id !== event.pointerId,
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
|
@ -1026,11 +1028,11 @@ export class App extends React.Component<any, AppState> {
|
|||
this.canvas?.removeEventListener("wheel", this.handleWheel);
|
||||
}
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -1052,8 +1054,8 @@ export class App extends React.Component<any, AppState> {
|
|||
this.canvasOnlyActions.includes(action),
|
||||
),
|
||||
],
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
top: event.clientY,
|
||||
left: event.clientX,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -1078,38 +1080,38 @@ export class App extends React.Component<any, AppState> {
|
|||
action => !this.canvasOnlyActions.includes(action),
|
||||
),
|
||||
],
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
top: event.clientY,
|
||||
left: event.clientX,
|
||||
});
|
||||
}}
|
||||
onPointerDown={e => {
|
||||
onPointerDown={event => {
|
||||
if (lastPointerUp !== null) {
|
||||
// Unfortunately, sometimes we don't get a pointerup after a pointerdown,
|
||||
// this can happen when a contextual menu or alert is triggered. In order to avoid
|
||||
// being in a weird state, we clean up on the next pointerdown
|
||||
lastPointerUp(e);
|
||||
lastPointerUp(event);
|
||||
}
|
||||
|
||||
if (isPanning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ lastPointerDownWith: e.pointerType });
|
||||
this.setState({ lastPointerDownWith: event.pointerType });
|
||||
|
||||
// pan canvas on wheel button drag or space+drag
|
||||
if (
|
||||
gesture.pointers.length === 0 &&
|
||||
(e.button === POINTER_BUTTON.WHEEL ||
|
||||
(e.button === POINTER_BUTTON.MAIN && isHoldingSpace))
|
||||
(event.button === POINTER_BUTTON.WHEEL ||
|
||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
|
||||
) {
|
||||
isPanning = true;
|
||||
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
||||
let { clientX: lastX, clientY: lastY } = e;
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const deltaX = lastX - e.clientX;
|
||||
const deltaY = lastY - e.clientY;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
let { clientX: lastX, clientY: lastY } = event;
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
const deltaX = lastX - event.clientX;
|
||||
const deltaY = lastY - event.clientY;
|
||||
lastX = event.clientX;
|
||||
lastY = event.clientY;
|
||||
|
||||
this.setState({
|
||||
scrollX: normalizeScroll(
|
||||
|
@ -1140,16 +1142,16 @@ export class App extends React.Component<any, AppState> {
|
|||
|
||||
// only handle left mouse button or touch
|
||||
if (
|
||||
e.button !== POINTER_BUTTON.MAIN &&
|
||||
e.button !== POINTER_BUTTON.TOUCH
|
||||
event.button !== POINTER_BUTTON.MAIN &&
|
||||
event.button !== POINTER_BUTTON.TOUCH
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
gesture.pointers.push({
|
||||
id: e.pointerId,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
id: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
if (gesture.pointers.length === 2) {
|
||||
gesture.lastCenter = getCenter(gesture.pointers);
|
||||
|
@ -1158,7 +1160,7 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
|
||||
// fixes pointermove causing selection of UI texts #32
|
||||
e.preventDefault();
|
||||
event.preventDefault();
|
||||
// Preventing the event above disables default behavior
|
||||
// of defocusing potentially focused element, which is what we
|
||||
// want when clicking inside the canvas.
|
||||
|
@ -1177,15 +1179,15 @@ export class App extends React.Component<any, AppState> {
|
|||
isOverVerticalScrollBar,
|
||||
} = isOverScrollBars(
|
||||
elements,
|
||||
e.clientX / window.devicePixelRatio,
|
||||
e.clientY / window.devicePixelRatio,
|
||||
event.clientX / window.devicePixelRatio,
|
||||
event.clientY / window.devicePixelRatio,
|
||||
canvasWidth / window.devicePixelRatio,
|
||||
canvasHeight / window.devicePixelRatio,
|
||||
this.state,
|
||||
);
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -1224,7 +1226,7 @@ export class App extends React.Component<any, AppState> {
|
|||
elements,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
e.pointerType,
|
||||
event.pointerType,
|
||||
);
|
||||
|
||||
const selectedElements = getSelectedElements(elements);
|
||||
|
@ -1248,7 +1250,7 @@ export class App extends React.Component<any, AppState> {
|
|||
this.state.zoom,
|
||||
);
|
||||
// clear selection if shift is not clicked
|
||||
if (!hitElement?.isSelected && !e.shiftKey) {
|
||||
if (!hitElement?.isSelected && !event.shiftKey) {
|
||||
elements = clearSelection(elements);
|
||||
}
|
||||
|
||||
|
@ -1265,7 +1267,7 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
|
||||
// We duplicate the selected element if alt is pressed on pointer down
|
||||
if (e.altKey) {
|
||||
if (event.altKey) {
|
||||
elements = [
|
||||
...elements.map(element => ({
|
||||
...element,
|
||||
|
@ -1294,9 +1296,9 @@ export class App extends React.Component<any, AppState> {
|
|||
if (elementIsAddedToSelection) {
|
||||
element = hitElement!;
|
||||
}
|
||||
let textX = e.clientX;
|
||||
let textY = e.clientY;
|
||||
if (!e.altKey) {
|
||||
let textX = event.clientX;
|
||||
let textY = event.clientY;
|
||||
if (!event.altKey) {
|
||||
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
|
||||
x,
|
||||
y,
|
||||
|
@ -1394,8 +1396,8 @@ export class App extends React.Component<any, AppState> {
|
|||
let lastY = y;
|
||||
|
||||
if (isOverHorizontalScrollBar || isOverVerticalScrollBar) {
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
lastX = event.clientX;
|
||||
lastY = event.clientY;
|
||||
}
|
||||
|
||||
let resizeArrowFn:
|
||||
|
@ -1466,14 +1468,14 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const target = e.target;
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOverHorizontalScrollBar) {
|
||||
const x = e.clientX;
|
||||
const x = event.clientX;
|
||||
const dx = x - lastX;
|
||||
this.setState({
|
||||
scrollX: normalizeScroll(
|
||||
|
@ -1485,7 +1487,7 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
|
||||
if (isOverVerticalScrollBar) {
|
||||
const y = e.clientY;
|
||||
const y = event.clientY;
|
||||
const dy = y - lastY;
|
||||
this.setState({
|
||||
scrollY: normalizeScroll(
|
||||
|
@ -1506,7 +1508,7 @@ export class App extends React.Component<any, AppState> {
|
|||
this.state.elementType === "line")
|
||||
) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -1521,7 +1523,7 @@ export class App extends React.Component<any, AppState> {
|
|||
const selectedElements = getSelectedElements(elements);
|
||||
if (selectedElements.length === 1) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -1549,13 +1551,13 @@ export class App extends React.Component<any, AppState> {
|
|||
deltaY,
|
||||
x,
|
||||
y,
|
||||
e.shiftKey,
|
||||
event.shiftKey,
|
||||
);
|
||||
} else {
|
||||
element.width -= deltaX;
|
||||
element.x += deltaX;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (event.shiftKey) {
|
||||
element.y += element.height - element.width;
|
||||
element.height = element.width;
|
||||
} else {
|
||||
|
@ -1581,11 +1583,11 @@ export class App extends React.Component<any, AppState> {
|
|||
deltaY,
|
||||
x,
|
||||
y,
|
||||
e.shiftKey,
|
||||
event.shiftKey,
|
||||
);
|
||||
} else {
|
||||
element.width += deltaX;
|
||||
if (e.shiftKey) {
|
||||
if (event.shiftKey) {
|
||||
element.y += element.height - element.width;
|
||||
element.height = element.width;
|
||||
} else {
|
||||
|
@ -1611,12 +1613,12 @@ export class App extends React.Component<any, AppState> {
|
|||
deltaY,
|
||||
x,
|
||||
y,
|
||||
e.shiftKey,
|
||||
event.shiftKey,
|
||||
);
|
||||
} else {
|
||||
element.width -= deltaX;
|
||||
element.x += deltaX;
|
||||
if (e.shiftKey) {
|
||||
if (event.shiftKey) {
|
||||
element.height = element.width;
|
||||
} else {
|
||||
element.height += deltaY;
|
||||
|
@ -1640,10 +1642,10 @@ export class App extends React.Component<any, AppState> {
|
|||
deltaY,
|
||||
x,
|
||||
y,
|
||||
e.shiftKey,
|
||||
event.shiftKey,
|
||||
);
|
||||
} else {
|
||||
if (e.shiftKey) {
|
||||
if (event.shiftKey) {
|
||||
element.width += deltaX;
|
||||
element.height = element.width;
|
||||
} else {
|
||||
|
@ -1748,7 +1750,7 @@ export class App extends React.Component<any, AppState> {
|
|||
const selectedElements = getSelectedElements(elements);
|
||||
if (selectedElements.length > 0) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -1772,7 +1774,7 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -1790,7 +1792,7 @@ export class App extends React.Component<any, AppState> {
|
|||
let dx = x - draggingElement.x;
|
||||
let dy = y - draggingElement.y;
|
||||
|
||||
if (e.shiftKey && points.length === 2) {
|
||||
if (event.shiftKey && points.length === 2) {
|
||||
({ width: dx, height: dy } = getPerfectElementSize(
|
||||
this.state.elementType,
|
||||
dx,
|
||||
|
@ -1806,7 +1808,7 @@ export class App extends React.Component<any, AppState> {
|
|||
pnt[1] = dy;
|
||||
}
|
||||
} else {
|
||||
if (e.shiftKey) {
|
||||
if (event.shiftKey) {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
this.state.elementType,
|
||||
width,
|
||||
|
@ -1828,7 +1830,7 @@ export class App extends React.Component<any, AppState> {
|
|||
draggingElement.shape = null;
|
||||
|
||||
if (this.state.elementType === "selection") {
|
||||
if (!e.shiftKey && isSomeElementSelected(elements)) {
|
||||
if (!event.shiftKey && isSomeElementSelected(elements)) {
|
||||
elements = clearSelection(elements);
|
||||
}
|
||||
const elementsWithinSelection = getElementsWithinSelection(
|
||||
|
@ -1842,7 +1844,7 @@ export class App extends React.Component<any, AppState> {
|
|||
this.setState({});
|
||||
};
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
const {
|
||||
draggingElement,
|
||||
resizingElement,
|
||||
|
@ -1869,7 +1871,7 @@ export class App extends React.Component<any, AppState> {
|
|||
}
|
||||
if (!draggingOccurred && draggingElement && !multiElement) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -1940,7 +1942,7 @@ export class App extends React.Component<any, AppState> {
|
|||
!draggingOccurred &&
|
||||
!elementIsAddedToSelection
|
||||
) {
|
||||
if (e.shiftKey) {
|
||||
if (event.shiftKey) {
|
||||
hitElement.isSelected = false;
|
||||
} else {
|
||||
elements = clearSelection(elements);
|
||||
|
@ -1984,11 +1986,11 @@ export class App extends React.Component<any, AppState> {
|
|||
window.addEventListener("pointermove", onPointerMove);
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
}}
|
||||
onDoubleClick={e => {
|
||||
onDoubleClick={event => {
|
||||
resetCursor();
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -2021,8 +2023,8 @@ export class App extends React.Component<any, AppState> {
|
|||
|
||||
this.setState({ editingElement: element });
|
||||
|
||||
let textX = e.clientX;
|
||||
let textY = e.clientY;
|
||||
let textX = event.clientX;
|
||||
let textY = event.clientY;
|
||||
|
||||
if (elementAtPosition && isTextElement(elementAtPosition)) {
|
||||
elements = elements.filter(
|
||||
|
@ -2050,7 +2052,7 @@ export class App extends React.Component<any, AppState> {
|
|||
// x and y will change after calling newTextElement function
|
||||
element.x = centerElementX;
|
||||
element.y = centerElementY;
|
||||
} else if (!e.altKey) {
|
||||
} else if (!event.altKey) {
|
||||
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
|
||||
x,
|
||||
y,
|
||||
|
@ -2099,15 +2101,15 @@ export class App extends React.Component<any, AppState> {
|
|||
},
|
||||
});
|
||||
}}
|
||||
onPointerMove={e => {
|
||||
gesture.pointers = gesture.pointers.map(p =>
|
||||
p.id === e.pointerId
|
||||
onPointerMove={event => {
|
||||
gesture.pointers = gesture.pointers.map(pointer =>
|
||||
pointer.id === event.pointerId
|
||||
? {
|
||||
id: e.pointerId,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
id: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
}
|
||||
: p,
|
||||
: pointer,
|
||||
);
|
||||
|
||||
if (gesture.pointers.length === 2) {
|
||||
|
@ -2135,10 +2137,10 @@ export class App extends React.Component<any, AppState> {
|
|||
if (isHoldingSpace || isPanning) {
|
||||
return;
|
||||
}
|
||||
const hasDeselectedButton = Boolean(e.buttons);
|
||||
const hasDeselectedButton = Boolean(event.buttons);
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
e,
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
);
|
||||
|
@ -2168,7 +2170,7 @@ export class App extends React.Component<any, AppState> {
|
|||
elements,
|
||||
{ x, y },
|
||||
this.state.zoom,
|
||||
e.pointerType,
|
||||
event.pointerType,
|
||||
);
|
||||
if (resizeElement && resizeElement.resizeHandle) {
|
||||
document.documentElement.style.cursor = getCursorForResizingElement(
|
||||
|
@ -2187,14 +2189,14 @@ export class App extends React.Component<any, AppState> {
|
|||
}}
|
||||
onPointerUp={this.removePointer}
|
||||
onPointerLeave={this.removePointer}
|
||||
onDrop={e => {
|
||||
const file = e.dataTransfer.files[0];
|
||||
onDrop={event => {
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file?.type === "application/json") {
|
||||
loadFromBlob(file)
|
||||
.then(({ elements, appState }) =>
|
||||
this.syncActionResult({ elements, appState }),
|
||||
)
|
||||
.catch(err => console.error(err));
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -2205,11 +2207,11 @@ export class App extends React.Component<any, AppState> {
|
|||
);
|
||||
}
|
||||
|
||||
private handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const { deltaX, deltaY } = e;
|
||||
private handleWheel = (event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
const { deltaX, deltaY } = event;
|
||||
|
||||
if (e[KEYS.META]) {
|
||||
if (event[KEYS.META]) {
|
||||
const sign = Math.sign(deltaY);
|
||||
const MAX_STEP = 10;
|
||||
let delta = Math.abs(deltaY);
|
||||
|
@ -2342,7 +2344,7 @@ class TopErrorBoundary extends React.Component<any, TopErrorBoundaryState> {
|
|||
for (const [key, value] of Object.entries({ ...localStorage })) {
|
||||
try {
|
||||
_localStorage[key] = JSON.parse(value);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
_localStorage[key] = value;
|
||||
}
|
||||
}
|
||||
|
@ -2366,8 +2368,8 @@ class TopErrorBoundary extends React.Component<any, TopErrorBoundaryState> {
|
|||
try {
|
||||
const StackTrace = await import("stacktrace-js");
|
||||
stack += (await StackTrace.fromError(error)).join("\n");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
stack += error.stack || "";
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue