mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-14 22:42:14 -07:00
Restore filters on load correctly.
This commit is contained in:
parent
b823ad2d9d
commit
aeb24fa58e
10 changed files with 196 additions and 28 deletions
|
|
@ -89,11 +89,14 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter(const QString &filena
|
|||
filterModel->blockSignals(true);
|
||||
filterModel->filterTree()->blockSignals(true);
|
||||
for (const QJsonValue &value : filtersArray) {
|
||||
qDebug() << value;
|
||||
if (value.isObject()) {
|
||||
QJsonObject filterObj = value.toObject();
|
||||
CardFilter *filter = CardFilter::fromJson(filterObj);
|
||||
if (filter)
|
||||
if (filter) {
|
||||
qDebug() << "Adding filter " << filter->type() << filter->attr() << filter->term();
|
||||
filterModel->addFilter(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
filterModel->blockSignals(false);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTimer>
|
||||
|
||||
VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWidget(QWidget *parent,
|
||||
FilterTreeModel *_filterModel)
|
||||
|
|
@ -35,6 +36,9 @@ VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWi
|
|||
toggleButton->setCheckable(true);
|
||||
layout->addWidget(toggleButton);
|
||||
connect(toggleButton, &QPushButton::toggled, this, &VisualDatabaseDisplayMainTypeFilterWidget::updateFilterMode);
|
||||
connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() {
|
||||
QTimer::singleShot(100, this, &VisualDatabaseDisplayMainTypeFilterWidget::syncWithFilterModel);
|
||||
});
|
||||
|
||||
createMainTypeButtons(); // Populate buttons initially
|
||||
updateFilterMode(false); // Initialize toggle button text
|
||||
|
|
@ -94,7 +98,7 @@ void VisualDatabaseDisplayMainTypeFilterWidget::handleMainTypeToggled(const QStr
|
|||
void VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeFilter()
|
||||
{
|
||||
// Clear existing filters related to main type
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrType);
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrMainType);
|
||||
|
||||
if (exactMatchMode) {
|
||||
// Exact Match: Only selected main types are allowed
|
||||
|
|
@ -110,7 +114,7 @@ void VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeFilter()
|
|||
for (const auto &type : selectedTypes) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrType));
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrMainType));
|
||||
}
|
||||
|
||||
// Exclude any other types (TypeAndNot)
|
||||
|
|
@ -118,7 +122,7 @@ void VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeFilter()
|
|||
if (!selectedTypes.contains(type)) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrType));
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrMainType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +132,7 @@ void VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeFilter()
|
|||
if (activeMainTypes[type]) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrType));
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrMainType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,3 +144,40 @@ void VisualDatabaseDisplayMainTypeFilterWidget::updateFilterMode(bool checked)
|
|||
toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes"));
|
||||
updateMainTypeFilter();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayMainTypeFilterWidget::syncWithFilterModel()
|
||||
{
|
||||
// Temporarily block signals for each button to prevent toggling while updating button states
|
||||
for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) {
|
||||
it.value()->blockSignals(true);
|
||||
}
|
||||
|
||||
// Uncheck all buttons
|
||||
for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) {
|
||||
it.value()->setChecked(false);
|
||||
}
|
||||
|
||||
// Get active filters for main types
|
||||
QSet<QString> activeTypes;
|
||||
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrMainType)) {
|
||||
if (filter->type() == CardFilter::Type::TypeAnd) {
|
||||
activeTypes.insert(filter->term());
|
||||
}
|
||||
}
|
||||
|
||||
// Check the buttons for active types
|
||||
for (const auto &type : activeTypes) {
|
||||
activeMainTypes[type] = true;
|
||||
if (typeButtons.contains(type)) {
|
||||
typeButtons[type]->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable signal emissions for each button
|
||||
for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) {
|
||||
it.value()->blockSignals(false);
|
||||
}
|
||||
|
||||
// Update the visibility of buttons
|
||||
updateMainTypeButtonsVisibility();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public:
|
|||
void handleMainTypeToggled(const QString &mainType, bool active);
|
||||
void updateMainTypeFilter();
|
||||
void updateFilterMode(bool checked);
|
||||
void syncWithFilterModel();
|
||||
|
||||
private:
|
||||
FilterTreeModel *filterModel;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi
|
|||
layout->addWidget(loadFromDeckButton);
|
||||
|
||||
connect(loadFromDeckButton, &QPushButton::clicked, this, &VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck);
|
||||
connect(filterModel, &FilterTreeModel::layoutChanged, this,
|
||||
[this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); });
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
||||
|
|
@ -86,9 +88,11 @@ void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name
|
|||
void VisualDatabaseDisplayNameFilterWidget::removeNameFilter(const QString &name)
|
||||
{
|
||||
if (activeFilters.contains(name)) {
|
||||
delete activeFilters[name]; // Remove button
|
||||
activeFilters[name]->deleteLater(); // Safe deletion
|
||||
activeFilters.remove(name);
|
||||
updateFilterModel();
|
||||
|
||||
QTimer::singleShot(0, this,
|
||||
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,3 +116,37 @@ void VisualDatabaseDisplayNameFilterWidget::updateFilterModel()
|
|||
filterModel->filterTree()->nodeAt(0)->nodeChanged();
|
||||
emit filterModel->layoutChanged();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel()
|
||||
{
|
||||
QStringList currentFilters;
|
||||
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::Attr::AttrName)) {
|
||||
if (filter->type() == CardFilter::Type::TypeOr) {
|
||||
currentFilters.append(filter->term());
|
||||
}
|
||||
}
|
||||
|
||||
QStringList activeFilterList = activeFilters.keys();
|
||||
|
||||
// Sort lists for efficient comparison
|
||||
std::sort(currentFilters.begin(), currentFilters.end());
|
||||
std::sort(activeFilterList.begin(), activeFilterList.end());
|
||||
|
||||
if (currentFilters == activeFilterList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove filters that are in the UI but not in the model
|
||||
for (const auto &name : activeFilterList) {
|
||||
if (!currentFilters.contains(name)) {
|
||||
removeNameFilter(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Add filters that are in the model but not in the UI
|
||||
for (const auto &name : currentFilters) {
|
||||
if (!activeFilters.contains(name)) {
|
||||
createNameFilter(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public:
|
|||
void removeNameFilter(const QString &name);
|
||||
void updateFilterList();
|
||||
void updateFilterModel();
|
||||
void syncWithFilterModel();
|
||||
|
||||
private:
|
||||
AbstractTabDeckEditor *deckEditor;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
|
||||
VisualDatabaseDisplaySetFilterWidget::VisualDatabaseDisplaySetFilterWidget(QWidget *parent,
|
||||
|
|
@ -32,6 +33,8 @@ VisualDatabaseDisplaySetFilterWidget::VisualDatabaseDisplaySetFilterWidget(QWidg
|
|||
toggleButton->setCheckable(true);
|
||||
layout->addWidget(toggleButton);
|
||||
connect(toggleButton, &QPushButton::toggled, this, &VisualDatabaseDisplaySetFilterWidget::updateFilterMode);
|
||||
connect(filterModel, &FilterTreeModel::layoutChanged, this,
|
||||
[this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplaySetFilterWidget::syncWithFilterModel); });
|
||||
|
||||
createSetButtons(); // Populate buttons initially
|
||||
updateFilterMode(false); // Initialize toggle button text
|
||||
|
|
@ -130,6 +133,43 @@ void VisualDatabaseDisplaySetFilterWidget::updateSetFilter()
|
|||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplaySetFilterWidget::syncWithFilterModel()
|
||||
{
|
||||
// Clear activeSets
|
||||
activeSets.clear();
|
||||
|
||||
// Read the current filters in filterModel
|
||||
auto currentFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrSet);
|
||||
|
||||
// Determine if we're in Exact Match mode or Includes mode
|
||||
QSet<QString> selectedSets;
|
||||
QSet<QString> excludedSets;
|
||||
for (const auto &filter : currentFilters) {
|
||||
if (filter->type() == CardFilter::Type::TypeAnd) {
|
||||
selectedSets.insert(filter->term());
|
||||
} else if (filter->type() == CardFilter::Type::TypeAndNot) {
|
||||
excludedSets.insert(filter->term());
|
||||
} else if (filter->type() == CardFilter::Type::TypeOr) {
|
||||
selectedSets.insert(filter->term());
|
||||
}
|
||||
}
|
||||
|
||||
// Determine exact match mode based on filter structure
|
||||
bool newExactMatchMode = !excludedSets.isEmpty();
|
||||
|
||||
if (newExactMatchMode != exactMatchMode) {
|
||||
exactMatchMode = newExactMatchMode;
|
||||
toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes"));
|
||||
}
|
||||
|
||||
// Sync button states with selected sets
|
||||
for (auto it = setButtons.begin(); it != setButtons.end(); ++it) {
|
||||
bool active = selectedSets.contains(it.key());
|
||||
activeSets[it.key()] = active;
|
||||
it.value()->setChecked(active);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplaySetFilterWidget::updateFilterMode(bool checked)
|
||||
{
|
||||
exactMatchMode = checked;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public:
|
|||
void handleSetToggled(const QString &setShortName, bool active);
|
||||
|
||||
void updateSetFilter();
|
||||
void syncWithFilterModel();
|
||||
void updateFilterMode(bool checked);
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTimer>
|
||||
|
||||
VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidget(QWidget *parent,
|
||||
FilterTreeModel *_filterModel)
|
||||
|
|
@ -18,7 +19,7 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg
|
|||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
// Create the spinbox
|
||||
// Create and setup the spinbox
|
||||
spinBox = new QSpinBox(this);
|
||||
spinBox->setMinimum(1);
|
||||
spinBox->setMaximum(getMaxSubTypeCount());
|
||||
|
|
@ -27,7 +28,7 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg
|
|||
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||
&VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility);
|
||||
|
||||
// Create the search box
|
||||
// Create search box
|
||||
searchBox = new QLineEdit(this);
|
||||
searchBox->setPlaceholderText(tr("Search subtypes..."));
|
||||
layout->addWidget(searchBox);
|
||||
|
|
@ -38,19 +39,22 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg
|
|||
flowWidget->setMaximumHeight(300);
|
||||
layout->addWidget(flowWidget);
|
||||
|
||||
// Create the toggle button for Exact Match/Includes mode
|
||||
// Toggle button setup (Exact Match / Includes mode)
|
||||
toggleButton = new QPushButton(this);
|
||||
toggleButton->setCheckable(true);
|
||||
layout->addWidget(toggleButton);
|
||||
connect(toggleButton, &QPushButton::toggled, this, &VisualDatabaseDisplaySubTypeFilterWidget::updateFilterMode);
|
||||
connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() {
|
||||
QTimer::singleShot(100, this, &VisualDatabaseDisplaySubTypeFilterWidget::syncWithFilterModel);
|
||||
});
|
||||
|
||||
createSubTypeButtons(); // Populate buttons initially
|
||||
updateFilterMode(false);
|
||||
createSubTypeButtons(); // Populate buttons initially
|
||||
updateFilterMode(false); // Initialize the toggle button text
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplaySubTypeFilterWidget::createSubTypeButtons()
|
||||
{
|
||||
// Iterate through main types and create buttons
|
||||
// Iterate through sub types and create buttons
|
||||
for (auto it = allSubCardTypesWithCount.begin(); it != allSubCardTypesWithCount.end(); ++it) {
|
||||
auto *button = new QPushButton(it.key(), flowWidget);
|
||||
button->setCheckable(true);
|
||||
|
|
@ -60,9 +64,9 @@ void VisualDatabaseDisplaySubTypeFilterWidget::createSubTypeButtons()
|
|||
flowWidget->addWidget(button);
|
||||
typeButtons[it.key()] = button;
|
||||
|
||||
// Connect toggle signal
|
||||
// Connect toggle signal for each button
|
||||
connect(button, &QPushButton::toggled, this,
|
||||
[this, mainType = it.key()](bool checked) { handleSubTypeToggled(mainType, checked); });
|
||||
[this, subType = it.key()](bool checked) { handleSubTypeToggled(subType, checked); });
|
||||
}
|
||||
updateSubTypeButtonsVisibility(); // Ensure visibility is updated initially
|
||||
}
|
||||
|
|
@ -72,6 +76,7 @@ void VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility()
|
|||
int threshold = spinBox->value();
|
||||
QString filterText = searchBox->text().trimmed().toLower();
|
||||
|
||||
// Iterate through buttons and hide/disable those below the threshold
|
||||
for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) {
|
||||
QString subType = it.key().toLower();
|
||||
bool isActive = activeSubTypes.value(it.key(), false);
|
||||
|
|
@ -92,12 +97,12 @@ int VisualDatabaseDisplaySubTypeFilterWidget::getMaxSubTypeCount() const
|
|||
return maxCount;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplaySubTypeFilterWidget::handleSubTypeToggled(const QString &mainType, bool active)
|
||||
void VisualDatabaseDisplaySubTypeFilterWidget::handleSubTypeToggled(const QString &subType, bool active)
|
||||
{
|
||||
activeSubTypes[mainType] = active;
|
||||
activeSubTypes[subType] = active;
|
||||
|
||||
if (typeButtons.contains(mainType)) {
|
||||
typeButtons[mainType]->setChecked(active);
|
||||
if (typeButtons.contains(subType)) {
|
||||
typeButtons[subType]->setChecked(active);
|
||||
}
|
||||
|
||||
updateSubTypeFilter();
|
||||
|
|
@ -105,11 +110,11 @@ void VisualDatabaseDisplaySubTypeFilterWidget::handleSubTypeToggled(const QStrin
|
|||
|
||||
void VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeFilter()
|
||||
{
|
||||
// Clear existing filters related to main type
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrType);
|
||||
// Clear existing filters related to sub types
|
||||
filterModel->clearFiltersOfType(CardFilter::Attr::AttrSubType);
|
||||
|
||||
if (exactMatchMode) {
|
||||
// Exact Match: Only selected main types are allowed
|
||||
// Exact Match: Only selected sub types are allowed
|
||||
QSet<QString> selectedTypes;
|
||||
for (const auto &type : activeSubTypes.keys()) {
|
||||
if (activeSubTypes[type]) {
|
||||
|
|
@ -118,11 +123,11 @@ void VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeFilter()
|
|||
}
|
||||
|
||||
if (!selectedTypes.isEmpty()) {
|
||||
// Require all selected types (TypeAnd)
|
||||
// Require all selected subtypes (TypeAnd)
|
||||
for (const auto &type : selectedTypes) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrType));
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrSubType));
|
||||
}
|
||||
|
||||
// Exclude any other types (TypeAndNot)
|
||||
|
|
@ -130,17 +135,17 @@ void VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeFilter()
|
|||
if (!selectedTypes.contains(type)) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrType));
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrSubType));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default Includes Mode (TypeOr) - match any selected main types
|
||||
// Default Includes Mode (TypeOr) - match any selected subtypes
|
||||
for (const auto &type : activeSubTypes.keys()) {
|
||||
if (activeSubTypes[type]) {
|
||||
QString typeString = type;
|
||||
filterModel->addFilter(
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrType));
|
||||
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrSubType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -152,3 +157,40 @@ void VisualDatabaseDisplaySubTypeFilterWidget::updateFilterMode(bool checked)
|
|||
toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes"));
|
||||
updateSubTypeFilter();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplaySubTypeFilterWidget::syncWithFilterModel()
|
||||
{
|
||||
// Temporarily block signals for each button to prevent toggling while updating button states
|
||||
for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) {
|
||||
it.value()->blockSignals(true);
|
||||
}
|
||||
|
||||
// Uncheck all buttons
|
||||
for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) {
|
||||
it.value()->setChecked(false);
|
||||
}
|
||||
|
||||
// Get active filters for sub types
|
||||
QSet<QString> activeTypes;
|
||||
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrSubType)) {
|
||||
if (filter->type() == CardFilter::Type::TypeAnd) {
|
||||
activeTypes.insert(filter->term());
|
||||
}
|
||||
}
|
||||
|
||||
// Check the buttons for active types
|
||||
for (const auto &type : activeTypes) {
|
||||
activeSubTypes[type] = true;
|
||||
if (typeButtons.contains(type)) {
|
||||
typeButtons[type]->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable signal emissions for each button
|
||||
for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) {
|
||||
it.value()->blockSignals(false);
|
||||
}
|
||||
|
||||
// Update the visibility of buttons
|
||||
updateSubTypeButtonsVisibility();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public:
|
|||
void handleSubTypeToggled(const QString &mainType, bool active);
|
||||
void updateSubTypeFilter();
|
||||
void updateFilterMode(bool checked);
|
||||
void syncWithFilterModel();
|
||||
|
||||
private:
|
||||
FilterTreeModel *filterModel;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ private:
|
|||
enum Attr a;
|
||||
|
||||
public:
|
||||
CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr){};
|
||||
CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) {};
|
||||
|
||||
Type type() const
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue