mirror of
https://github.com/eclipse-cdt/cdt
synced 2025-08-01 21:35:40 +02:00
bug 289608: [Scanner Discovery] Discovery options does not offer profiles defined in supertypes in project properties
This commit is contained in:
parent
b0529877b9
commit
a16d5c8cbd
5 changed files with 135 additions and 65 deletions
|
@ -10,15 +10,24 @@
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
package org.eclipse.cdt.build.internal.core.scannerconfig;
|
package org.eclipse.cdt.build.internal.core.scannerconfig;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
import org.eclipse.cdt.build.core.scannerconfig.CfgInfoContext;
|
import org.eclipse.cdt.build.core.scannerconfig.CfgInfoContext;
|
||||||
import org.eclipse.cdt.managedbuilder.core.IInputType;
|
import org.eclipse.cdt.managedbuilder.core.IInputType;
|
||||||
import org.eclipse.cdt.managedbuilder.core.IResourceInfo;
|
import org.eclipse.cdt.managedbuilder.core.IResourceInfo;
|
||||||
import org.eclipse.cdt.managedbuilder.core.ITool;
|
import org.eclipse.cdt.managedbuilder.core.ITool;
|
||||||
|
import org.eclipse.cdt.managedbuilder.core.IToolChain;
|
||||||
import org.eclipse.cdt.managedbuilder.internal.core.Configuration;
|
import org.eclipse.cdt.managedbuilder.internal.core.Configuration;
|
||||||
import org.eclipse.cdt.managedbuilder.internal.core.FolderInfo;
|
import org.eclipse.cdt.managedbuilder.internal.core.FolderInfo;
|
||||||
|
import org.eclipse.cdt.managedbuilder.internal.core.InputType;
|
||||||
|
import org.eclipse.cdt.managedbuilder.internal.core.ManagedMakeMessages;
|
||||||
import org.eclipse.cdt.managedbuilder.internal.core.ResourceConfiguration;
|
import org.eclipse.cdt.managedbuilder.internal.core.ResourceConfiguration;
|
||||||
import org.eclipse.cdt.managedbuilder.internal.core.Tool;
|
import org.eclipse.cdt.managedbuilder.internal.core.Tool;
|
||||||
import org.eclipse.cdt.managedbuilder.internal.core.ToolChain;
|
import org.eclipse.cdt.managedbuilder.internal.core.ToolChain;
|
||||||
|
import org.eclipse.core.runtime.Assert;
|
||||||
|
|
||||||
|
import com.ibm.icu.text.MessageFormat;
|
||||||
|
|
||||||
public class CfgScannerConfigUtil {
|
public class CfgScannerConfigUtil {
|
||||||
public static CfgInfoContext adjustPerRcTypeContext(CfgInfoContext context){
|
public static CfgInfoContext adjustPerRcTypeContext(CfgInfoContext context){
|
||||||
|
@ -136,4 +145,100 @@ public class CfgScannerConfigUtil {
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for toolchain's discovery profiles. Discovery profiles could be
|
||||||
|
* specified on toolchain level, input types level or in their super-classes.
|
||||||
|
*
|
||||||
|
* @param toolchain - toolchain to search for scanner discovery profiles.
|
||||||
|
* @return all available discovery profiles in given toolchain
|
||||||
|
*/
|
||||||
|
public static Set<String> getAllScannerDiscoveryProfileIds(IToolChain toolchain) {
|
||||||
|
Assert.isNotNull(toolchain);
|
||||||
|
|
||||||
|
Set<String> profiles = new TreeSet<String>();
|
||||||
|
|
||||||
|
if (toolchain!=null) {
|
||||||
|
String toolchainProfileId = toolchain.getScannerConfigDiscoveryProfileId();
|
||||||
|
if (toolchainProfileId!=null && toolchainProfileId.length()>0) {
|
||||||
|
profiles.add(toolchainProfileId);
|
||||||
|
}
|
||||||
|
ITool[] tools = toolchain.getTools();
|
||||||
|
for (ITool tool : tools) {
|
||||||
|
profiles.addAll(getAllScannerDiscoveryProfileIds(tool));
|
||||||
|
}
|
||||||
|
IToolChain superClass = toolchain.getSuperClass();
|
||||||
|
if (superClass!=null) {
|
||||||
|
profiles.addAll(getAllScannerDiscoveryProfileIds(superClass));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for tool's discovery profiles. Discovery profiles could be retrieved
|
||||||
|
* from tool/input type super-class. Input type could hold list of profiles
|
||||||
|
* separated by pipe character '|'.
|
||||||
|
*
|
||||||
|
* @param tool - tool to search for scanner discovery profiles
|
||||||
|
* @return all available discovery profiles in given configuration
|
||||||
|
*/
|
||||||
|
public static Set<String> getAllScannerDiscoveryProfileIds(ITool tool) {
|
||||||
|
Assert.isNotNull(tool);
|
||||||
|
|
||||||
|
if ( ! (tool instanceof Tool) ) {
|
||||||
|
String msg = MessageFormat.format(ManagedMakeMessages.getString("CfgScannerConfigUtil_ErrorNotSupported"), //$NON-NLS-1$
|
||||||
|
new String[] { Tool.class.getName() });
|
||||||
|
throw new UnsupportedOperationException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> profiles = new TreeSet<String>();
|
||||||
|
|
||||||
|
for (IInputType inputType : ((Tool) tool).getAllInputTypes()) {
|
||||||
|
for (String profileId : getAllScannerDiscoveryProfileIds(inputType)) {
|
||||||
|
profiles.add(profileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ITool superClass = tool.getSuperClass();
|
||||||
|
if (superClass!=null) {
|
||||||
|
profiles.addAll(getAllScannerDiscoveryProfileIds(superClass));
|
||||||
|
}
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for input type's discovery profiles. Discovery profiles could be specified
|
||||||
|
* on input type super-class. Input type could hold list of profiles
|
||||||
|
* separated by pipe character '|'.
|
||||||
|
*
|
||||||
|
* @param inputType - input type to search for scanner discovery profiles
|
||||||
|
* @return all available discovery profiles in given configuration
|
||||||
|
*/
|
||||||
|
private static Set<String> getAllScannerDiscoveryProfileIds(IInputType inputType) {
|
||||||
|
Assert.isNotNull(inputType);
|
||||||
|
|
||||||
|
if ( ! (inputType instanceof InputType) ) {
|
||||||
|
String msg = MessageFormat.format(ManagedMakeMessages.getString("CfgScannerConfigUtil_ErrorNotSupported"), //$NON-NLS-1$
|
||||||
|
new String[] { InputType.class.getName() });
|
||||||
|
throw new UnsupportedOperationException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> profiles = new TreeSet<String>();
|
||||||
|
|
||||||
|
String attribute = ((InputType) inputType).getDiscoveryProfileIdAttribute();
|
||||||
|
if (attribute!=null) {
|
||||||
|
// FIXME: temporary; we should add new method to IInputType instead of that
|
||||||
|
for (String profileId : attribute.split("\\|")) { //$NON-NLS-1$
|
||||||
|
profiles.add(profileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IInputType superClass = inputType.getSuperClass();
|
||||||
|
if (superClass!=null) {
|
||||||
|
profiles.addAll(getAllScannerDiscoveryProfileIds(superClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1812,6 +1812,9 @@ public class InputType extends BuildObject implements IInputType {
|
||||||
if(getDiscoveryProfileIdAttribute() != null)
|
if(getDiscoveryProfileIdAttribute() != null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (superClass!=null && superClass instanceof InputType)
|
||||||
|
return ((InputType)superClass).hasScannerConfigSettings();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,6 +138,7 @@ ManagedBuilderCorePlugin.resourceChangeHandlingInitializationJob=Initializing Re
|
||||||
#Internal Builder messages
|
#Internal Builder messages
|
||||||
InternalBuilder.msg.header=Internal Builder: {0}
|
InternalBuilder.msg.header=Internal Builder: {0}
|
||||||
InternalBuilder.nothing.todo=Nothing to be done for project {0}
|
InternalBuilder.nothing.todo=Nothing to be done for project {0}
|
||||||
|
CfgScannerConfigUtil_ErrorNotSupported=Only type {0} is supported in this method.
|
||||||
CleanFilesAction.cleanFiles=Clean File(s)
|
CleanFilesAction.cleanFiles=Clean File(s)
|
||||||
CleanFilesAction.cleanSelectedFiles=Clean the selected file(s).
|
CleanFilesAction.cleanSelectedFiles=Clean the selected file(s).
|
||||||
CleanFilesAction.cleaningFiles=Cleaning files
|
CleanFilesAction.cleaningFiles=Cleaning files
|
||||||
|
|
|
@ -3877,7 +3877,13 @@ public class Tool extends HoldsOptions implements ITool, IOptionCategory, IMatch
|
||||||
|
|
||||||
public boolean hasScannerConfigSettings(IInputType type){
|
public boolean hasScannerConfigSettings(IInputType type){
|
||||||
if(type == null){
|
if(type == null){
|
||||||
return hasScannerConfigSettings();
|
boolean has = hasScannerConfigSettings();
|
||||||
|
if (has)
|
||||||
|
return has;
|
||||||
|
ITool superClass = getSuperClass();
|
||||||
|
if (superClass!=null && superClass instanceof Tool)
|
||||||
|
return ((Tool)superClass).hasScannerConfigSettings(type);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return ((InputType)type).hasScannerConfigSettings();
|
return ((InputType)type).hasScannerConfigSettings();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,12 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.SortedSet;
|
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
import org.eclipse.cdt.build.core.scannerconfig.CfgInfoContext;
|
import org.eclipse.cdt.build.core.scannerconfig.CfgInfoContext;
|
||||||
import org.eclipse.cdt.build.core.scannerconfig.ICfgScannerConfigBuilderInfo2Set;
|
import org.eclipse.cdt.build.core.scannerconfig.ICfgScannerConfigBuilderInfo2Set;
|
||||||
import org.eclipse.cdt.build.internal.core.scannerconfig.CfgDiscoveredPathManager;
|
import org.eclipse.cdt.build.internal.core.scannerconfig.CfgDiscoveredPathManager;
|
||||||
|
import org.eclipse.cdt.build.internal.core.scannerconfig.CfgScannerConfigUtil;
|
||||||
import org.eclipse.cdt.build.internal.core.scannerconfig2.CfgScannerConfigProfileManager;
|
import org.eclipse.cdt.build.internal.core.scannerconfig2.CfgScannerConfigProfileManager;
|
||||||
import org.eclipse.cdt.core.model.util.CDTListComparator;
|
import org.eclipse.cdt.core.model.util.CDTListComparator;
|
||||||
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
|
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
|
||||||
|
@ -38,8 +38,6 @@ import org.eclipse.cdt.managedbuilder.core.IInputType;
|
||||||
import org.eclipse.cdt.managedbuilder.core.IResourceInfo;
|
import org.eclipse.cdt.managedbuilder.core.IResourceInfo;
|
||||||
import org.eclipse.cdt.managedbuilder.core.ITool;
|
import org.eclipse.cdt.managedbuilder.core.ITool;
|
||||||
import org.eclipse.cdt.managedbuilder.core.IToolChain;
|
import org.eclipse.cdt.managedbuilder.core.IToolChain;
|
||||||
import org.eclipse.cdt.managedbuilder.internal.core.InputType;
|
|
||||||
import org.eclipse.cdt.managedbuilder.internal.core.Tool;
|
|
||||||
import org.eclipse.cdt.ui.CUIPlugin;
|
import org.eclipse.cdt.ui.CUIPlugin;
|
||||||
import org.eclipse.cdt.ui.newui.CDTPrefUtil;
|
import org.eclipse.cdt.ui.newui.CDTPrefUtil;
|
||||||
import org.eclipse.cdt.ui.newui.UIMessages;
|
import org.eclipse.cdt.ui.newui.UIMessages;
|
||||||
|
@ -93,7 +91,7 @@ public class DiscoveryTab extends AbstractCBuildPropertyTab implements IBuildInf
|
||||||
private Composite profileOptionsComposite;
|
private Composite profileOptionsComposite;
|
||||||
|
|
||||||
private ICfgScannerConfigBuilderInfo2Set cbi;
|
private ICfgScannerConfigBuilderInfo2Set cbi;
|
||||||
private Map<InfoContext, Object> baseInfoMap;
|
private Map<InfoContext, IScannerConfigBuilderInfo2> baseInfoMap;
|
||||||
private IScannerConfigBuilderInfo2 buildInfo;
|
private IScannerConfigBuilderInfo2 buildInfo;
|
||||||
private CfgInfoContext iContext;
|
private CfgInfoContext iContext;
|
||||||
private List<DiscoveryProfilePageConfiguration> pagesList = null;
|
private List<DiscoveryProfilePageConfiguration> pagesList = null;
|
||||||
|
@ -260,32 +258,32 @@ public class DiscoveryTab extends AbstractCBuildPropertyTab implements IBuildInf
|
||||||
scopeComboBox.select(selScope);
|
scopeComboBox.select(selScope);
|
||||||
fTableDefinition.setText(lblText);
|
fTableDefinition.setText(lblText);
|
||||||
|
|
||||||
Map<CfgInfoContext, IScannerConfigBuilderInfo2> m = cbi.getInfoMap();
|
Map<CfgInfoContext, IScannerConfigBuilderInfo2> infoMap = cbi.getInfoMap();
|
||||||
int pos = resTable.getSelectionIndex();
|
int pos = resTable.getSelectionIndex();
|
||||||
resTable.removeAll();
|
resTable.removeAll();
|
||||||
for (CfgInfoContext ic : m.keySet()) {
|
for (CfgInfoContext cfgInfoContext : infoMap.keySet()) {
|
||||||
String s = null;
|
String s = null;
|
||||||
IResourceInfo rci = ic.getResourceInfo();
|
IResourceInfo rcInfo = cfgInfoContext.getResourceInfo();
|
||||||
if (rci == null) { // per configuration
|
if (rcInfo == null) { // per configuration
|
||||||
s = ic.getConfiguration().getName();
|
s = cfgInfoContext.getConfiguration().getName();
|
||||||
} else { // per resource
|
} else { // per resource
|
||||||
if (!configPath.equals(rci.getPath()))
|
if (!configPath.equals(rcInfo.getPath()))
|
||||||
continue;
|
continue;
|
||||||
IInputType typ = ic.getInputType();
|
IInputType typ = cfgInfoContext.getInputType();
|
||||||
if (typ != null)
|
if (typ != null)
|
||||||
s = typ.getName();
|
s = typ.getName();
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
ITool tool = ic.getTool();
|
ITool tool = cfgInfoContext.getTool();
|
||||||
if (tool != null)
|
if (tool != null)
|
||||||
s = tool.getName();
|
s = tool.getName();
|
||||||
}
|
}
|
||||||
if (s == null)
|
if (s == null)
|
||||||
s = Messages.getString("DiscoveryTab.3"); //$NON-NLS-1$
|
s = Messages.getString("DiscoveryTab.3"); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
IScannerConfigBuilderInfo2 bi2 = m.get(ic);
|
IScannerConfigBuilderInfo2 bi2 = infoMap.get(cfgInfoContext);
|
||||||
TableItem ti = new TableItem(resTable, SWT.NONE);
|
TableItem ti = new TableItem(resTable, SWT.NONE);
|
||||||
ti.setText(s);
|
ti.setText(s);
|
||||||
ti.setData("cont", ic); //$NON-NLS-1$
|
ti.setData("cont", cfgInfoContext); //$NON-NLS-1$
|
||||||
ti.setData("info", bi2); //$NON-NLS-1$
|
ti.setData("info", bi2); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
int len = resTable.getItemCount();
|
int len = resTable.getItemCount();
|
||||||
|
@ -341,7 +339,6 @@ public class DiscoveryTab extends AbstractCBuildPropertyTab implements IBuildInf
|
||||||
profileComboBox.removeAll();
|
profileComboBox.removeAll();
|
||||||
List<String> profilesList = buildInfo.getProfileIdList();
|
List<String> profilesList = buildInfo.getProfileIdList();
|
||||||
Collections.sort(profilesList, CDTListComparator.getInstance());
|
Collections.sort(profilesList, CDTListComparator.getInstance());
|
||||||
visibleProfilesList = new ArrayList<String>(profilesList.size());
|
|
||||||
|
|
||||||
if (realPages != null && realPages.length > 0) {
|
if (realPages != null && realPages.length > 0) {
|
||||||
for (AbstractDiscoveryPage realPage : realPages) {
|
for (AbstractDiscoveryPage realPage : realPages) {
|
||||||
|
@ -361,8 +358,8 @@ public class DiscoveryTab extends AbstractCBuildPropertyTab implements IBuildInf
|
||||||
// property page
|
// property page
|
||||||
if (!needPerRcProfile) {
|
if (!needPerRcProfile) {
|
||||||
// configuration-wide (all in tool-chain)
|
// configuration-wide (all in tool-chain)
|
||||||
IConfiguration conf = iContext.getConfiguration();
|
IConfiguration cfg = iContext.getConfiguration();
|
||||||
IToolChain toolchain = conf!=null ? conf.getToolChain() : null;
|
IToolChain toolchain = cfg!=null ? cfg.getToolChain() : null;
|
||||||
|
|
||||||
if (toolchain==null) {
|
if (toolchain==null) {
|
||||||
ManagedBuilderUIPlugin.log(new Status(IStatus.ERROR, ManagedBuilderUIPlugin.getUniqueIdentifier(),
|
ManagedBuilderUIPlugin.log(new Status(IStatus.ERROR, ManagedBuilderUIPlugin.getUniqueIdentifier(),
|
||||||
|
@ -374,28 +371,24 @@ public class DiscoveryTab extends AbstractCBuildPropertyTab implements IBuildInf
|
||||||
// for generic Makefile project let user choose any profile
|
// for generic Makefile project let user choose any profile
|
||||||
contextProfiles = new TreeSet<String>(profilesList);
|
contextProfiles = new TreeSet<String>(profilesList);
|
||||||
} else {
|
} else {
|
||||||
contextProfiles = getAllScannerDiscoveryProfileIds(toolchain);
|
contextProfiles = CfgScannerConfigUtil.getAllScannerDiscoveryProfileIds(toolchain);
|
||||||
}
|
}
|
||||||
if (contextProfiles.size()==0) {
|
if (contextProfiles.size()==0) {
|
||||||
// GCC profile is a sensible default for user to start with
|
// GCC profile is a sensible default for user to start with
|
||||||
visibleProfilesList.add(0,GCC_PER_PROJECT_PROFILE);
|
contextProfiles.add(GCC_PER_PROJECT_PROFILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// per language (i.e. input type)
|
// per language (i.e. input type)
|
||||||
Tool tool = (Tool) iContext.getTool();
|
ITool tool = iContext.getTool();
|
||||||
if (tool==null)
|
if (tool==null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
contextProfiles = getAllScannerDiscoveryProfileIds(tool);
|
contextProfiles = CfgScannerConfigUtil.getAllScannerDiscoveryProfileIds(tool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String profileId : contextProfiles) {
|
visibleProfilesList = new ArrayList<String>(contextProfiles);
|
||||||
if (profilesList.contains(profileId)
|
|
||||||
&& needPerRcProfile==CfgScannerConfigProfileManager.isPerFileProfile(profileId))
|
|
||||||
visibleProfilesList.add(profileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
realPages = new AbstractDiscoveryPage[visibleProfilesList.size()];
|
realPages = new AbstractDiscoveryPage[visibleProfilesList.size()];
|
||||||
String[] labels = new String[visibleProfilesList.size()];
|
String[] labels = new String[visibleProfilesList.size()];
|
||||||
|
@ -433,44 +426,6 @@ public class DiscoveryTab extends AbstractCBuildPropertyTab implements IBuildInf
|
||||||
handleDiscoveryProfileChanged();
|
handleDiscoveryProfileChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> getAllScannerDiscoveryProfileIds(ITool tool) {
|
|
||||||
SortedSet<String> profiles = new TreeSet<String>();
|
|
||||||
|
|
||||||
for (IInputType inputType : ((Tool) tool).getAllInputTypes()) {
|
|
||||||
for (String profileId : getDiscoveryProfileIds(inputType)) {
|
|
||||||
profiles.add(profileId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return profiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> getAllScannerDiscoveryProfileIds(IToolChain toolchain) {
|
|
||||||
SortedSet<String> profiles = new TreeSet<String>();
|
|
||||||
|
|
||||||
if (toolchain!=null) {
|
|
||||||
String toolchainProfileId = toolchain.getScannerConfigDiscoveryProfileId();
|
|
||||||
if (toolchainProfileId!=null && toolchainProfileId.length()>0) {
|
|
||||||
profiles.add(toolchainProfileId);
|
|
||||||
}
|
|
||||||
ITool[] tools = toolchain.getTools();
|
|
||||||
for (ITool tool : tools) {
|
|
||||||
profiles.addAll(getAllScannerDiscoveryProfileIds(tool));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return profiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] getDiscoveryProfileIds(IInputType it) {
|
|
||||||
String attribute = ((InputType) it).getDiscoveryProfileIdAttribute();
|
|
||||||
if (null == attribute)
|
|
||||||
return new String[0];
|
|
||||||
// FIXME: temporary; we should add new method to IInputType instead of
|
|
||||||
// that
|
|
||||||
String[] profileIds = attribute.split("\\|"); //$NON-NLS-1$
|
|
||||||
for (int i = 0; i < profileIds.length; i++)
|
|
||||||
profileIds[i] = profileIds[i].trim();
|
|
||||||
return profileIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] normalize(String[] labels, String[] ids, int counter) {
|
private String[] normalize(String[] labels, String[] ids, int counter) {
|
||||||
int mode = CDTPrefUtil.getInt(CDTPrefUtil.KEY_DISC_NAMES);
|
int mode = CDTPrefUtil.getInt(CDTPrefUtil.KEY_DISC_NAMES);
|
||||||
|
|
Loading…
Add table
Reference in a new issue