mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-22 09:30:14 +01:00
This commit is contained in:
parent
e141544791
commit
55f377c388
2 changed files with 76 additions and 103 deletions
|
|
@ -55,7 +55,7 @@ export const openMenuPanel = (options: {
|
||||||
const avID = options.blockElement.getAttribute("data-av-id");
|
const avID = options.blockElement.getAttribute("data-av-id");
|
||||||
fetchPost("/api/av/renderAttributeView", {
|
fetchPost("/api/av/renderAttributeView", {
|
||||||
id: avID,
|
id: avID,
|
||||||
query: (options.blockElement.querySelector('[data-type="av-search"]') as HTMLInputElement)?.value ||"",
|
query: (options.blockElement.querySelector('[data-type="av-search"]') as HTMLInputElement)?.value || "",
|
||||||
pageSize: parseInt(options.blockElement.getAttribute("data-page-size")) || undefined,
|
pageSize: parseInt(options.blockElement.getAttribute("data-page-size")) || undefined,
|
||||||
viewID: options.blockElement.getAttribute(Constants.CUSTOM_SY_AV_VIEW)
|
viewID: options.blockElement.getAttribute(Constants.CUSTOM_SY_AV_VIEW)
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
|
|
@ -81,7 +81,7 @@ export const openMenuPanel = (options: {
|
||||||
} else if (options.type === "filters") {
|
} else if (options.type === "filters") {
|
||||||
html = getFiltersHTML(data.view);
|
html = getFiltersHTML(data.view);
|
||||||
} else if (options.type === "select") {
|
} else if (options.type === "select") {
|
||||||
html = getSelectHTML(data.view, options.cellElements);
|
html = getSelectHTML(data.view, options.cellElements, true);
|
||||||
} else if (options.type === "asset") {
|
} else if (options.type === "asset") {
|
||||||
html = getAssetHTML(options.cellElements);
|
html = getAssetHTML(options.cellElements);
|
||||||
} else if (options.type === "edit") {
|
} else if (options.type === "edit") {
|
||||||
|
|
@ -1127,7 +1127,7 @@ export const openMenuPanel = (options: {
|
||||||
break;
|
break;
|
||||||
} else if (type === "addColOptionOrCell") {
|
} else if (type === "addColOptionOrCell") {
|
||||||
if (target.querySelector(".b3-menu__checked")) {
|
if (target.querySelector(".b3-menu__checked")) {
|
||||||
removeCellOption(options.protyle, data, options.cellElements, menuElement.querySelector(`.b3-chips .b3-chip[data-content="${escapeAttr(target.dataset.name)}"]`), options.blockElement);
|
removeCellOption(options.protyle, options.cellElements, menuElement.querySelector(`.b3-chips .b3-chip[data-content="${escapeAttr(target.dataset.name)}"]`), options.blockElement);
|
||||||
} else {
|
} else {
|
||||||
addColOptionOrCell(options.protyle, data, options.cellElements, target, menuElement, options.blockElement);
|
addColOptionOrCell(options.protyle, data, options.cellElements, target, menuElement, options.blockElement);
|
||||||
}
|
}
|
||||||
|
|
@ -1136,7 +1136,7 @@ export const openMenuPanel = (options: {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
break;
|
break;
|
||||||
} else if (type === "removeCellOption") {
|
} else if (type === "removeCellOption") {
|
||||||
removeCellOption(options.protyle, data, options.cellElements, target.parentElement, options.blockElement);
|
removeCellOption(options.protyle, options.cellElements, target.parentElement, options.blockElement);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ import {bindEditEvent, getEditHTML} from "./col";
|
||||||
import {updateAttrViewCellAnimation} from "./action";
|
import {updateAttrViewCellAnimation} from "./action";
|
||||||
import {genAVValueHTML} from "./blockAttr";
|
import {genAVValueHTML} from "./blockAttr";
|
||||||
import {escapeAttr} from "../../../util/escape";
|
import {escapeAttr} from "../../../util/escape";
|
||||||
import {genCellValueByElement} from "./cell";
|
import {genCellValueByElement, getTypeByCellElement} from "./cell";
|
||||||
|
|
||||||
|
let cellValues: IAVCellValue[];
|
||||||
|
|
||||||
const filterSelectHTML = (key: string, options: { name: string, color: string }[], selected: string[] = []) => {
|
const filterSelectHTML = (key: string, options: { name: string, color: string }[], selected: string[] = []) => {
|
||||||
let html = "";
|
let html = "";
|
||||||
|
|
@ -53,7 +55,7 @@ const filterSelectHTML = (key: string, options: { name: string, color: string }[
|
||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeCellOption = (protyle: IProtyle, data: IAV, cellElements: HTMLElement[], target: HTMLElement, blockElement: Element) => {
|
export const removeCellOption = (protyle: IProtyle, cellElements: HTMLElement[], target: HTMLElement, blockElement: Element) => {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -61,26 +63,19 @@ export const removeCellOption = (protyle: IProtyle, data: IAV, cellElements: HTM
|
||||||
const doOperations: IOperation[] = [];
|
const doOperations: IOperation[] = [];
|
||||||
const undoOperations: IOperation[] = [];
|
const undoOperations: IOperation[] = [];
|
||||||
let mSelectValue: IAVCellSelectValue[];
|
let mSelectValue: IAVCellSelectValue[];
|
||||||
|
const avID = blockElement.getAttribute("data-av-id")
|
||||||
cellElements.forEach((item, elementIndex) => {
|
cellElements.forEach((item, elementIndex) => {
|
||||||
if (!blockElement.contains(item)) {
|
if (!blockElement.contains(item)) {
|
||||||
item = cellElements[elementIndex] = blockElement.querySelector(`.av__cell[data-id="${item.dataset.id}"]`) as HTMLElement;
|
const rowElement = hasClosestByClassName(item, "av__row");
|
||||||
|
if (rowElement) {
|
||||||
|
item = cellElements[elementIndex] =
|
||||||
|
(blockElement.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${item.dataset.colId}"]`) ||
|
||||||
|
// block attr
|
||||||
|
blockElement.querySelector(`.fn__flex-1[data-col-id="${item.dataset.colId}"]`)) as HTMLElement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const rowID = (hasClosestByClassName(item, "av__row") as HTMLElement).dataset.id;
|
const rowID = (hasClosestByClassName(item, "av__row") as HTMLElement).dataset.id;
|
||||||
let cellValue: IAVCellValue;
|
const cellValue: IAVCellValue = cellValues[elementIndex];
|
||||||
data.view.rows.find(row => {
|
|
||||||
if (row.id === rowID) {
|
|
||||||
row.cells.find(cell => {
|
|
||||||
if (cell.value.keyID === item.dataset.colId) {
|
|
||||||
if (!cell.value.mSelect) {
|
|
||||||
cell.value.mSelect = [];
|
|
||||||
}
|
|
||||||
cellValue = cell.value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const oldValue = JSON.parse(JSON.stringify(cellValue));
|
const oldValue = JSON.parse(JSON.stringify(cellValue));
|
||||||
if (elementIndex === 0) {
|
if (elementIndex === 0) {
|
||||||
cellValue.mSelect?.find((item, index) => {
|
cellValue.mSelect?.find((item, index) => {
|
||||||
|
|
@ -98,7 +93,7 @@ export const removeCellOption = (protyle: IProtyle, data: IAV, cellElements: HTM
|
||||||
id: cellValue.id,
|
id: cellValue.id,
|
||||||
keyID: colId,
|
keyID: colId,
|
||||||
rowID,
|
rowID,
|
||||||
avID: data.id,
|
avID,
|
||||||
data: cellValue
|
data: cellValue
|
||||||
});
|
});
|
||||||
undoOperations.push({
|
undoOperations.push({
|
||||||
|
|
@ -106,7 +101,7 @@ export const removeCellOption = (protyle: IProtyle, data: IAV, cellElements: HTM
|
||||||
id: cellValue.id,
|
id: cellValue.id,
|
||||||
keyID: colId,
|
keyID: colId,
|
||||||
rowID,
|
rowID,
|
||||||
avID: data.id,
|
avID,
|
||||||
data: oldValue
|
data: oldValue
|
||||||
});
|
});
|
||||||
if (item.classList.contains("custom-attr__avvalue")) {
|
if (item.classList.contains("custom-attr__avvalue")) {
|
||||||
|
|
@ -187,28 +182,23 @@ export const setColOption = (protyle: IProtyle, data: IAV, target: HTMLElement,
|
||||||
menuElement.innerHTML = getEditHTML({protyle, data, colId, isCustomAttr});
|
menuElement.innerHTML = getEditHTML({protyle, data, colId, isCustomAttr});
|
||||||
bindEditEvent({protyle, data, menuElement, isCustomAttr, blockID});
|
bindEditEvent({protyle, data, menuElement, isCustomAttr, blockID});
|
||||||
} else {
|
} else {
|
||||||
cellElements.forEach((cellElement: HTMLMediaElement) => {
|
cellElements.forEach((cellElement: HTMLElement, index) => {
|
||||||
data.view.rows.find(row => {
|
const rowElement = hasClosestByClassName(cellElement, "av__row");
|
||||||
if (row.id === (hasClosestByClassName(cellElement, "av__row") as HTMLElement).dataset.id) {
|
if (rowElement) {
|
||||||
row.cells.find(cell => {
|
cellElement = cellElements[index] = (blockElement.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${cellElement.dataset.colId}"]`) ||
|
||||||
if (cell.id === cellElement.dataset.id) {
|
blockElement.querySelector(`.fn__flex-1[data-col-id="${cellElement.dataset.colId}"]`)) as HTMLElement;
|
||||||
cell.value.mSelect.find((item) => {
|
}
|
||||||
if (item.content === name) {
|
cellValues[index].mSelect.find((item) => {
|
||||||
item.content = inputElement.value;
|
if (item.content === name) {
|
||||||
return true;
|
item.content = inputElement.value;
|
||||||
}
|
|
||||||
});
|
|
||||||
if (cellElement.classList.contains("custom-attr__avvalue")) {
|
|
||||||
cellElement.innerHTML = genAVValueHTML(cell.value);
|
|
||||||
} else {
|
|
||||||
updateAttrViewCellAnimation(cellElement, cell.value);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (cellElement.classList.contains("custom-attr__avvalue")) {
|
||||||
|
cellElement.innerHTML = genAVValueHTML(cellValues[index]);
|
||||||
|
} else {
|
||||||
|
updateAttrViewCellAnimation(cellElement, cellValues[index]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
menuElement.innerHTML = getSelectHTML(data.view, cellElements);
|
menuElement.innerHTML = getSelectHTML(data.view, cellElements);
|
||||||
bindSelectEvent(protyle, data, menuElement, cellElements, blockElement);
|
bindSelectEvent(protyle, data, menuElement, cellElements, blockElement);
|
||||||
|
|
@ -265,28 +255,23 @@ export const setColOption = (protyle: IProtyle, data: IAV, target: HTMLElement,
|
||||||
menuElement.innerHTML = getEditHTML({protyle, data, colId, isCustomAttr});
|
menuElement.innerHTML = getEditHTML({protyle, data, colId, isCustomAttr});
|
||||||
bindEditEvent({protyle, data, menuElement, isCustomAttr, blockID});
|
bindEditEvent({protyle, data, menuElement, isCustomAttr, blockID});
|
||||||
} else {
|
} else {
|
||||||
cellElements.forEach((cellElement: HTMLElement) => {
|
cellElements.forEach((cellElement: HTMLElement, index) => {
|
||||||
data.view.rows.find(row => {
|
const rowElement = hasClosestByClassName(cellElement, "av__row");
|
||||||
if (row.id === (hasClosestByClassName(cellElement, "av__row") as HTMLElement).dataset.id) {
|
if (rowElement) {
|
||||||
row.cells.find(cell => {
|
cellElement = cellElements[index] = (blockElement.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${cellElement.dataset.colId}"]`) ||
|
||||||
if (cell.id === cellElement.dataset.id) {
|
blockElement.querySelector(`.fn__flex-1[data-col-id="${cellElement.dataset.colId}"]`)) as HTMLElement;
|
||||||
cell.value.mSelect.find((item, index) => {
|
}
|
||||||
if (item.content === newName) {
|
cellValues[index].mSelect.find((item, selectIndex) => {
|
||||||
cell.value.mSelect.splice(index, 1);
|
if (item.content === newName) {
|
||||||
return true;
|
cellValues[index].mSelect.splice(selectIndex, 1);
|
||||||
}
|
|
||||||
});
|
|
||||||
if (cellElement.classList.contains("custom-attr__avvalue")) {
|
|
||||||
cellElement.innerHTML = genAVValueHTML(cell.value);
|
|
||||||
} else {
|
|
||||||
updateAttrViewCellAnimation(cellElement, cell.value);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (cellElement.classList.contains("custom-attr__avvalue")) {
|
||||||
|
cellElement.innerHTML = genAVValueHTML(cellValues[index]);
|
||||||
|
} else {
|
||||||
|
updateAttrViewCellAnimation(cellElement, cellValues[index]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
menuElement.innerHTML = getSelectHTML(data.view, cellElements);
|
menuElement.innerHTML = getSelectHTML(data.view, cellElements);
|
||||||
bindSelectEvent(protyle, data, menuElement, cellElements, blockElement);
|
bindSelectEvent(protyle, data, menuElement, cellElements, blockElement);
|
||||||
|
|
@ -344,29 +329,24 @@ export const setColOption = (protyle: IProtyle, data: IAV, target: HTMLElement,
|
||||||
menuElement.innerHTML = getEditHTML({protyle, data, colId, isCustomAttr});
|
menuElement.innerHTML = getEditHTML({protyle, data, colId, isCustomAttr});
|
||||||
bindEditEvent({protyle, data, menuElement, isCustomAttr, blockID});
|
bindEditEvent({protyle, data, menuElement, isCustomAttr, blockID});
|
||||||
} else {
|
} else {
|
||||||
cellElements.forEach((cellElement: HTMLElement) => {
|
cellElements.forEach((cellElement: HTMLElement, cellIndex) => {
|
||||||
data.view.rows.find(row => {
|
const rowElement = hasClosestByClassName(cellElement, "av__row");
|
||||||
if (row.id === (hasClosestByClassName(cellElement, "av__row") as HTMLElement).dataset.id) {
|
if (rowElement) {
|
||||||
row.cells.find(cell => {
|
cellElement = cellElements[cellIndex] = (blockElement.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${cellElement.dataset.colId}"]`) ||
|
||||||
if (cell.id === cellElement.dataset.id) {
|
blockElement.querySelector(`.fn__flex-1[data-col-id="${cellElement.dataset.colId}"]`)) as HTMLElement;
|
||||||
cell.value.mSelect.find((item) => {
|
}
|
||||||
if (item.content === name) {
|
cellValues[cellIndex].mSelect.find((item) => {
|
||||||
item.content = inputElement.value;
|
if (item.content === name) {
|
||||||
item.color = (index + 1).toString();
|
item.content = inputElement.value;
|
||||||
return true;
|
item.color = (index + 1).toString();
|
||||||
}
|
|
||||||
});
|
|
||||||
if (cellElement.classList.contains("custom-attr__avvalue")) {
|
|
||||||
cellElement.innerHTML = genAVValueHTML(cell.value);
|
|
||||||
} else {
|
|
||||||
updateAttrViewCellAnimation(cellElement, cell.value);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (cellElement.classList.contains("custom-attr__avvalue")) {
|
||||||
|
cellElement.innerHTML = genAVValueHTML(cellValues[cellIndex]);
|
||||||
|
} else {
|
||||||
|
updateAttrViewCellAnimation(cellElement, cellValues[cellIndex]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
menuElement.innerHTML = getSelectHTML(data.view, cellElements);
|
menuElement.innerHTML = getSelectHTML(data.view, cellElements);
|
||||||
bindSelectEvent(protyle, data, menuElement, cellElements, blockElement);
|
bindSelectEvent(protyle, data, menuElement, cellElements, blockElement);
|
||||||
|
|
@ -421,12 +401,12 @@ export const bindSelectEvent = (protyle: IProtyle, data: IAV, menuElement: HTMLE
|
||||||
currentElement = menuElement.querySelector(".b3-menu__item--current");
|
currentElement = menuElement.querySelector(".b3-menu__item--current");
|
||||||
}
|
}
|
||||||
if (currentElement.querySelector(".b3-menu__checked")) {
|
if (currentElement.querySelector(".b3-menu__checked")) {
|
||||||
removeCellOption(protyle, data, cellElements, menuElement.querySelector(`.b3-chips .b3-chip[data-content="${escapeAttr(currentElement.dataset.name)}"]`), blockElement);
|
removeCellOption(protyle, cellElements, menuElement.querySelector(`.b3-chips .b3-chip[data-content="${escapeAttr(currentElement.dataset.name)}"]`), blockElement);
|
||||||
} else {
|
} else {
|
||||||
addColOptionOrCell(protyle, data, cellElements, currentElement, menuElement, blockElement);
|
addColOptionOrCell(protyle, data, cellElements, currentElement, menuElement, blockElement);
|
||||||
}
|
}
|
||||||
} else if (event.key === "Backspace" && inputElement.value === "") {
|
} else if (event.key === "Backspace" && inputElement.value === "") {
|
||||||
removeCellOption(protyle, data, cellElements, inputElement.previousElementSibling as HTMLElement, blockElement);
|
removeCellOption(protyle, cellElements, inputElement.previousElementSibling as HTMLElement, blockElement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -450,7 +430,7 @@ export const addColOptionOrCell = (protyle: IProtyle, data: IAV, cellElements: H
|
||||||
const rowElement = hasClosestByClassName(item, "av__row");
|
const rowElement = hasClosestByClassName(item, "av__row");
|
||||||
if (rowElement) {
|
if (rowElement) {
|
||||||
cellElements[index] = (blockElement.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${item.dataset.colId}"]`) ||
|
cellElements[index] = (blockElement.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${item.dataset.colId}"]`) ||
|
||||||
blockElement.querySelector(`.fn__flex-1[data-col-id="${item.dataset.colId}"]`) ) as HTMLElement;
|
blockElement.querySelector(`.fn__flex-1[data-col-id="${item.dataset.colId}"]`)) as HTMLElement;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -474,23 +454,8 @@ export const addColOptionOrCell = (protyle: IProtyle, data: IAV, cellElements: H
|
||||||
if (!itemRowElement) {
|
if (!itemRowElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cellValue: IAVCellValue;
|
|
||||||
const rowID = itemRowElement.dataset.id;
|
const rowID = itemRowElement.dataset.id;
|
||||||
// 快速选中后如果 render 了再使用 genCellValueByElement 获取的元素和当前选中的不一致, https://github.com/siyuan-note/siyuan/issues/11268
|
const cellValue: IAVCellValue = cellValues[index];
|
||||||
data.view.rows.find(row => {
|
|
||||||
if (row.id === rowID) {
|
|
||||||
row.cells.find(cell => {
|
|
||||||
if (cell.value.keyID === item.dataset.colId) {
|
|
||||||
if (!cell.value.mSelect) {
|
|
||||||
cell.value.mSelect = [];
|
|
||||||
}
|
|
||||||
cellValue = cell.value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const oldValue = JSON.parse(JSON.stringify(cellValue));
|
const oldValue = JSON.parse(JSON.stringify(cellValue));
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
if (colData.type === "mSelect") {
|
if (colData.type === "mSelect") {
|
||||||
|
|
@ -569,7 +534,15 @@ export const addColOptionOrCell = (protyle: IProtyle, data: IAV, cellElements: H
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSelectHTML = (data: IAVTable, cellElements: HTMLElement[]) => {
|
export const getSelectHTML = (data: IAVTable, cellElements: HTMLElement[], init = false) => {
|
||||||
|
if (init) {
|
||||||
|
// 快速选中后如果 render 了再使用 genCellValueByElement 获取的元素和当前选中的不一致, https://github.com/siyuan-note/siyuan/issues/11268
|
||||||
|
cellValues = [];
|
||||||
|
const isCustomAttr = cellElements[0].classList.contains("custom-attr__avvalue");
|
||||||
|
cellElements.forEach(item => {
|
||||||
|
cellValues.push(genCellValueByElement(isCustomAttr ? item.dataset.type as TAVCol : getTypeByCellElement(item), item));
|
||||||
|
});
|
||||||
|
}
|
||||||
const colId = cellElements[0].dataset["colId"];
|
const colId = cellElements[0].dataset["colId"];
|
||||||
const colData = data.columns.find(item => {
|
const colData = data.columns.find(item => {
|
||||||
if (item.id === colId) {
|
if (item.id === colId) {
|
||||||
|
|
@ -579,7 +552,7 @@ export const getSelectHTML = (data: IAVTable, cellElements: HTMLElement[]) => {
|
||||||
|
|
||||||
let selectedHTML = "";
|
let selectedHTML = "";
|
||||||
const selected: string[] = [];
|
const selected: string[] = [];
|
||||||
genCellValueByElement(colData.type, cellElements[0]).mSelect?.forEach((item) => {
|
cellValues[0].mSelect?.forEach((item) => {
|
||||||
selected.push(item.content);
|
selected.push(item.content);
|
||||||
selectedHTML += `<div class="b3-chip b3-chip--middle" data-content="${escapeAttr(item.content)}" style="background-color:var(--b3-font-background${item.color});color:var(--b3-font-color${item.color})">${item.content}<svg class="b3-chip__close" data-type="removeCellOption"><use xlink:href="#iconCloseRound"></use></svg></div>`;
|
selectedHTML += `<div class="b3-chip b3-chip--middle" data-content="${escapeAttr(item.content)}" style="background-color:var(--b3-font-background${item.color});color:var(--b3-font-color${item.color})">${item.content}<svg class="b3-chip__close" data-type="removeCellOption"><use xlink:href="#iconCloseRound"></use></svg></div>`;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue