mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-07 17:49:17 +00:00
Implementing FormConfigElement.combine and associated tests.
git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@12111 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -28,6 +28,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
@@ -115,10 +116,123 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
/**
|
/**
|
||||||
* @see org.alfresco.config.ConfigElement#combine(org.alfresco.config.ConfigElement)
|
* @see org.alfresco.config.ConfigElement#combine(org.alfresco.config.ConfigElement)
|
||||||
*/
|
*/
|
||||||
public ConfigElement combine(ConfigElement configElement)
|
public ConfigElement combine(ConfigElement otherConfigElement)
|
||||||
{
|
{
|
||||||
// TODO Impl this.
|
FormConfigElement otherFormElem = (FormConfigElement)otherConfigElement;
|
||||||
throw new UnsupportedOperationException("This method not yet impl'd.");
|
FormConfigElement result = new FormConfigElement();
|
||||||
|
|
||||||
|
combineSubmissionURL(otherFormElem, result);
|
||||||
|
|
||||||
|
combineTemplates(otherFormElem, result);
|
||||||
|
|
||||||
|
combineFieldVisibilities(otherFormElem, result);
|
||||||
|
|
||||||
|
combineSets(otherFormElem, result);
|
||||||
|
|
||||||
|
//TODO Fields
|
||||||
|
|
||||||
|
//TODO field-controls
|
||||||
|
|
||||||
|
//TODO constraint-for-field
|
||||||
|
|
||||||
|
combineModelOverrides(otherFormElem, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void combineModelOverrides(FormConfigElement otherFormElem,
|
||||||
|
FormConfigElement result)
|
||||||
|
{
|
||||||
|
for (StringPair override : modelOverrides)
|
||||||
|
{
|
||||||
|
result.addModelOverrides(override.getName(), override.getValue());
|
||||||
|
}
|
||||||
|
for (StringPair override : otherFormElem.modelOverrides)
|
||||||
|
{
|
||||||
|
result.addModelOverrides(override.getName(), override.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void combineSets(FormConfigElement otherFormElem,
|
||||||
|
FormConfigElement result)
|
||||||
|
{
|
||||||
|
for (String nextOldSet : setIdentifiers)
|
||||||
|
{
|
||||||
|
FormSet nextOldSetData = sets.get(nextOldSet);
|
||||||
|
String setId = nextOldSetData.getSetId();
|
||||||
|
String parentId = nextOldSetData.getParentId();
|
||||||
|
String appearance = nextOldSetData.getAppearance();
|
||||||
|
result.addSet(setId, parentId, appearance);
|
||||||
|
}
|
||||||
|
for (String nextNewSet : otherFormElem.setIdentifiers)
|
||||||
|
{
|
||||||
|
FormSet nextNewSetData = otherFormElem.sets.get(nextNewSet);
|
||||||
|
String setId = nextNewSetData.getSetId();
|
||||||
|
String parentId = nextNewSetData.getParentId();
|
||||||
|
String appearance = nextNewSetData.getAppearance();
|
||||||
|
result.addSet(setId, parentId, appearance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void combineFieldVisibilities(FormConfigElement otherFormElem,
|
||||||
|
FormConfigElement result)
|
||||||
|
{
|
||||||
|
List<FieldVisibilityInstruction> combinedInstructions = new ArrayList<FieldVisibilityInstruction>();
|
||||||
|
combinedInstructions.addAll(this.visibilityInstructions);
|
||||||
|
|
||||||
|
//If there are already instructions pertaining to a particular
|
||||||
|
// field id, we can just leave them in place as the new should override the old.
|
||||||
|
//TODO Test this is true.
|
||||||
|
combinedInstructions.addAll(otherFormElem.visibilityInstructions);
|
||||||
|
for (FieldVisibilityInstruction fvi : combinedInstructions)
|
||||||
|
{
|
||||||
|
result.addFieldVisibility(fvi.isShow() ? "show" : "hide"
|
||||||
|
, fvi.getFieldId(), fvi.getModesAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void combineTemplates(FormConfigElement otherFormElem,
|
||||||
|
FormConfigElement result)
|
||||||
|
{
|
||||||
|
for (String s : this.createTemplates)
|
||||||
|
{
|
||||||
|
String reqsRole = this.rolesForCreateTemplates.get(s);
|
||||||
|
result.addFormTemplate("create-form", s, reqsRole);
|
||||||
|
}
|
||||||
|
for (String s : otherFormElem.createTemplates)
|
||||||
|
{
|
||||||
|
String reqsRole = otherFormElem.rolesForCreateTemplates.get(s);
|
||||||
|
result.addFormTemplate("create-form", s, reqsRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String s : this.editTemplates)
|
||||||
|
{
|
||||||
|
String reqsRole = this.rolesForEditTemplates.get(s);
|
||||||
|
result.addFormTemplate("edit-form", s, reqsRole);
|
||||||
|
}
|
||||||
|
for (String s : otherFormElem.editTemplates)
|
||||||
|
{
|
||||||
|
String reqsRole = otherFormElem.rolesForEditTemplates.get(s);
|
||||||
|
result.addFormTemplate("edit-form", s, reqsRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String s : this.viewTemplates)
|
||||||
|
{
|
||||||
|
String reqsRole = this.rolesForViewTemplates.get(s);
|
||||||
|
result.addFormTemplate("view-form", s, reqsRole);
|
||||||
|
}
|
||||||
|
for (String s : otherFormElem.viewTemplates)
|
||||||
|
{
|
||||||
|
String reqsRole = otherFormElem.rolesForViewTemplates.get(s);
|
||||||
|
result.addFormTemplate("view-form", s, reqsRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void combineSubmissionURL(FormConfigElement otherFormElem,
|
||||||
|
FormConfigElement result)
|
||||||
|
{
|
||||||
|
String otherSubmissionURL = otherFormElem.getSubmissionURL();
|
||||||
|
result.setSubmissionURL(otherSubmissionURL == null ? this.submissionURL : otherSubmissionURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSubmissionURL()
|
public String getSubmissionURL()
|
||||||
@@ -257,6 +371,21 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getCreateTemplates()
|
||||||
|
{
|
||||||
|
return Collections.unmodifiableList(createTemplates);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getEditTemplates()
|
||||||
|
{
|
||||||
|
return Collections.unmodifiableList(editTemplates);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getViewTemplates()
|
||||||
|
{
|
||||||
|
return Collections.unmodifiableList(viewTemplates);
|
||||||
|
}
|
||||||
|
|
||||||
public List<StringPair> getModelOverrideProperties()
|
public List<StringPair> getModelOverrideProperties()
|
||||||
{
|
{
|
||||||
return Collections.unmodifiableList(modelOverrides);
|
return Collections.unmodifiableList(modelOverrides);
|
||||||
@@ -276,18 +405,27 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nodeName.equals("create-form"))
|
if (nodeName.equals("create-form"))
|
||||||
|
{
|
||||||
|
if (!createTemplates.contains(template))
|
||||||
{
|
{
|
||||||
createTemplates.add(template);
|
createTemplates.add(template);
|
||||||
|
}
|
||||||
rolesForCreateTemplates.put(template, requiredRole);
|
rolesForCreateTemplates.put(template, requiredRole);
|
||||||
}
|
}
|
||||||
else if (nodeName.equals("edit-form"))
|
else if (nodeName.equals("edit-form"))
|
||||||
|
{
|
||||||
|
if (!editTemplates.contains(template))
|
||||||
{
|
{
|
||||||
editTemplates.add(template);
|
editTemplates.add(template);
|
||||||
|
}
|
||||||
rolesForEditTemplates.put(template, requiredRole);
|
rolesForEditTemplates.put(template, requiredRole);
|
||||||
}
|
}
|
||||||
else if (nodeName.equals("view-form"))
|
else if (nodeName.equals("view-form"))
|
||||||
|
{
|
||||||
|
if (!viewTemplates.contains(template))
|
||||||
{
|
{
|
||||||
viewTemplates.add(template);
|
viewTemplates.add(template);
|
||||||
|
}
|
||||||
rolesForViewTemplates.put(template, requiredRole);
|
rolesForViewTemplates.put(template, requiredRole);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -305,8 +443,11 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* package */void addSet(String setId, String parentSetId, String appearance)
|
/* package */void addSet(String setId, String parentSetId, String appearance)
|
||||||
|
{
|
||||||
|
if (!setIdentifiers.contains(setId))
|
||||||
{
|
{
|
||||||
setIdentifiers.add(setId);
|
setIdentifiers.add(setId);
|
||||||
|
}
|
||||||
sets.put(setId, new FormSet(setId, parentSetId, appearance));
|
sets.put(setId, new FormSet(setId, parentSetId, appearance));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +485,16 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
|
|
||||||
/* package */void addModelOverrides(String name, String value)
|
/* package */void addModelOverrides(String name, String value)
|
||||||
{
|
{
|
||||||
modelOverrides.add(new StringPair(name, value));
|
StringPair modelOverride = new StringPair(name, value);
|
||||||
|
//TODO Consider using a different collection type here.
|
||||||
|
for (Iterator<StringPair> iter = modelOverrides.iterator(); iter.hasNext(); )
|
||||||
|
{
|
||||||
|
if (iter.next().getName().equals(name))
|
||||||
|
{
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modelOverrides.add(modelOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FormSet
|
public static class FormSet
|
||||||
@@ -370,6 +520,27 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
{
|
{
|
||||||
return appearance;
|
return appearance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object otherObj)
|
||||||
|
{
|
||||||
|
if (otherObj == null
|
||||||
|
|| !otherObj.getClass().equals(this.getClass()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FormSet otherSet = (FormSet) otherObj;
|
||||||
|
return this.setId.equals(otherSet.setId)
|
||||||
|
&& this.parentId.equals(otherSet.parentId)
|
||||||
|
&& this.appearance.equals(otherSet.appearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return setId.hashCode()
|
||||||
|
+ 7 * parentId.hashCode() + 13 * appearance.hashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FormField
|
public static class FormField
|
||||||
@@ -478,6 +649,20 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
return Collections.unmodifiableList(forModes);
|
return Collections.unmodifiableList(forModes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getModesAsString()
|
||||||
|
{
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (Iterator<Mode> iter = forModes.iterator(); iter.hasNext(); )
|
||||||
|
{
|
||||||
|
result.append(iter.next());
|
||||||
|
if (iter.hasNext())
|
||||||
|
{
|
||||||
|
result.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
@@ -485,5 +670,27 @@ public class FormConfigElement extends ConfigElementAdapter
|
|||||||
result.append(" ").append(fieldId).append(" ").append(forModes);
|
result.append(" ").append(fieldId).append(" ").append(forModes);
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object otherObj)
|
||||||
|
{
|
||||||
|
if (otherObj == null
|
||||||
|
|| !otherObj.getClass().equals(this.getClass()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FieldVisibilityInstruction otherFVI = (FieldVisibilityInstruction) otherObj;
|
||||||
|
return this.show == otherFVI.show
|
||||||
|
&& this.fieldId.equals(otherFVI.fieldId)
|
||||||
|
&& this.forModes.equals(otherFVI.forModes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return show ? 1 : 0
|
||||||
|
+ 7 * fieldId.hashCode() + 13 * forModes.hashCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -24,6 +24,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco.web.config;
|
package org.alfresco.web.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.alfresco.config.Config;
|
import org.alfresco.config.Config;
|
||||||
import org.alfresco.config.ConfigElement;
|
import org.alfresco.config.ConfigElement;
|
||||||
import org.alfresco.config.xml.XMLConfigService;
|
import org.alfresco.config.xml.XMLConfigService;
|
||||||
@@ -31,29 +34,44 @@ import org.alfresco.util.BaseTest;
|
|||||||
import org.alfresco.web.config.DefaultControlsConfigElement.ControlParam;
|
import org.alfresco.web.config.DefaultControlsConfigElement.ControlParam;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JUnit tests to exercise the forms-related capabilities in to the web client config
|
* JUnit tests to exercise the forms-related capabilities in to the web client
|
||||||
* service. These only include those override-related tests that require multiple
|
* config service. These only include those override-related tests that require
|
||||||
* config xml files.
|
* multiple config xml files.
|
||||||
*
|
*
|
||||||
* @author Neil McErlean
|
* @author Neil McErlean
|
||||||
*/
|
*/
|
||||||
public class WebClientFormsOverridingTest extends BaseTest
|
public class WebClientFormsOverridingTest extends BaseTest
|
||||||
{
|
{
|
||||||
|
private XMLConfigService configService;
|
||||||
|
private Config globalConfig;
|
||||||
|
private FormConfigElement formConfigElement;
|
||||||
|
private static final String FORMS_CONFIG = "test-config-forms.xml";
|
||||||
|
private static final String FORMS_OVERRIDE_CONFIG = "test-config-forms-override.xml";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see junit.framework.TestCase#setUp()
|
* @see junit.framework.TestCase#setUp()
|
||||||
*/
|
*/
|
||||||
protected void setUp() throws Exception
|
protected void setUp() throws Exception
|
||||||
{
|
{
|
||||||
super.setUp();
|
super.setUp();
|
||||||
|
configService = initXMLConfigService(FORMS_CONFIG,
|
||||||
|
FORMS_OVERRIDE_CONFIG);
|
||||||
|
assertNotNull("configService was null.", configService);
|
||||||
|
globalConfig = configService.getGlobalConfig();
|
||||||
|
assertNotNull("Global config was null.", globalConfig);
|
||||||
|
|
||||||
|
Config contentConfig = configService.getConfig("content");
|
||||||
|
assertNotNull("contentConfig was null.", contentConfig);
|
||||||
|
|
||||||
|
ConfigElement confElement = contentConfig.getConfigElement("form");
|
||||||
|
assertNotNull("confElement was null.", confElement);
|
||||||
|
assertTrue("confElement should be instanceof FormConfigElement.",
|
||||||
|
confElement instanceof FormConfigElement);
|
||||||
|
formConfigElement = (FormConfigElement) confElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testDefaultControlsOverride()
|
public void testDefaultControlsOverride()
|
||||||
{
|
{
|
||||||
XMLConfigService svc = initXMLConfigService("test-config-forms.xml",
|
|
||||||
"test-config-forms-override.xml");
|
|
||||||
|
|
||||||
// get hold of the default-controls config from the global section
|
|
||||||
Config globalConfig = svc.getGlobalConfig();
|
|
||||||
ConfigElement globalDefaultControls = globalConfig
|
ConfigElement globalDefaultControls = globalConfig
|
||||||
.getConfigElement("default-controls");
|
.getConfigElement("default-controls");
|
||||||
assertNotNull("global default-controls element should not be null",
|
assertNotNull("global default-controls element should not be null",
|
||||||
@@ -74,11 +92,6 @@ public class WebClientFormsOverridingTest extends BaseTest
|
|||||||
|
|
||||||
public void testConstraintHandlersOverride()
|
public void testConstraintHandlersOverride()
|
||||||
{
|
{
|
||||||
XMLConfigService svc = initXMLConfigService("test-config-forms.xml",
|
|
||||||
"test-config-forms-override.xml");
|
|
||||||
|
|
||||||
// get hold of the constraint-handlers config from the global section
|
|
||||||
Config globalConfig = svc.getGlobalConfig();
|
|
||||||
ConfigElement globalConstraintHandlers = globalConfig
|
ConfigElement globalConstraintHandlers = globalConfig
|
||||||
.getConfigElement("constraint-handlers");
|
.getConfigElement("constraint-handlers");
|
||||||
assertNotNull("global constraint-handlers element should not be null",
|
assertNotNull("global constraint-handlers element should not be null",
|
||||||
@@ -97,4 +110,18 @@ public class WebClientFormsOverridingTest extends BaseTest
|
|||||||
assertEquals("Modified message is wrong.", "Overridden Message", chCE
|
assertEquals("Modified message is wrong.", "Overridden Message", chCE
|
||||||
.getMessageFor("NUMERIC"));
|
.getMessageFor("NUMERIC"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testFormSubmissionUrlAndModelOverridePropsOverride()
|
||||||
|
{
|
||||||
|
assertEquals("Submission URL was incorrect.", "overridden/submission/url",
|
||||||
|
formConfigElement.getSubmissionURL());
|
||||||
|
|
||||||
|
List<StringPair> expectedModelOverrideProperties = new ArrayList<StringPair>();
|
||||||
|
expectedModelOverrideProperties.add(new StringPair(
|
||||||
|
"fields.title.mandatory", "false"));
|
||||||
|
assertEquals("Expected property missing.",
|
||||||
|
expectedModelOverrideProperties, formConfigElement
|
||||||
|
.getModelOverrideProperties());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -12,13 +12,51 @@
|
|||||||
<control-param name="c">Never.</control-param>
|
<control-param name="c">Never.</control-param>
|
||||||
<control-param name="d"></control-param>
|
<control-param name="d"></control-param>
|
||||||
</type>
|
</type>
|
||||||
<type name="xyz" template="org/alfresco/xyz.ftl" />
|
<type name="xyz" template="org/alfresco/xyz.ftl">
|
||||||
|
</type>
|
||||||
</default-controls>
|
</default-controls>
|
||||||
<constraint-handlers>
|
<constraint-handlers>
|
||||||
<constraint type="REGEX" validation-handler="Alfresco.forms.validation.regexMatch" />
|
<constraint type="REGEX" validation-handler="Alfresco.forms.validation.regexMatch" />
|
||||||
<constraint type="NUMERIC" validation-handler="Alfresco.forms.validation.numericMatch"
|
<constraint type="NUMERIC" validation-handler="Alfresco.forms.validation.numericMatch"
|
||||||
message="Overridden Message" />
|
message="Overridden Message" message-id="regex_error" />
|
||||||
<constraint type="RANGE" validation-handler="Alfresco.forms.validation.rangeMatch" />
|
<constraint type="RANGE" validation-handler="Alfresco.forms.validation.rangeMatch" />
|
||||||
</constraint-handlers>
|
</constraint-handlers>
|
||||||
</config>
|
</config>
|
||||||
|
|
||||||
|
<!-- The evaluator would normally be node-type, but that makes testing difficult -->
|
||||||
|
<config evaluator="string-compare" condition="content">
|
||||||
|
<!-- For this example, I'm going to include all optional attributes -->
|
||||||
|
<form submission-url="overridden/submission/url">
|
||||||
|
<view-form template="/path/view/template" requires-role="Consumer" />
|
||||||
|
<edit-form template="/path/edit/template/manager" requires-role="Manager" />
|
||||||
|
<edit-form template="/path/edit/template/contributor" requires-role="Contributor" />
|
||||||
|
<create-form template="/path/create/template" requires-role="Manager" />
|
||||||
|
<field-visibility>
|
||||||
|
<!-- This comment is here to test the FormReader -->
|
||||||
|
<show id="name" />
|
||||||
|
<show id="title" for-mode="view, create" />
|
||||||
|
<hide id="quota" for-mode="edit" />
|
||||||
|
</field-visibility>
|
||||||
|
<appearance>
|
||||||
|
<set id="details" appearance="fieldset" />
|
||||||
|
<set id="user" parent="details" appearance="panel" />
|
||||||
|
|
||||||
|
<field id="name" label="Name" label-id="field_label_name" disabled="true" set="details"
|
||||||
|
help-text="This is the name of the node" help-text-id="field_help_name" >
|
||||||
|
<control template="alfresco/extension/formcontrols/my-name.ftl">
|
||||||
|
<control-param name="foo">bar</control-param>
|
||||||
|
</control>
|
||||||
|
<constraint-message type="REGEX" message="The name can not contain the character '{0}'"
|
||||||
|
message-id="field_error_name" />
|
||||||
|
</field>
|
||||||
|
<field id="title" label="My Title" set="details" />
|
||||||
|
<field id="username" set="user" />
|
||||||
|
<field id="quota" set="user" requires-role="Coordinator" />
|
||||||
|
</appearance>
|
||||||
|
<model-override>
|
||||||
|
<property name="fields.title.mandatory">false</property>
|
||||||
|
</model-override>
|
||||||
|
</form>
|
||||||
|
</config>
|
||||||
|
|
||||||
</alfresco-config>
|
</alfresco-config>
|
Reference in New Issue
Block a user