Skip to content
Snippets Groups Projects
Commit 0cd56200 authored by Sebastian Listl's avatar Sebastian Listl :speech_balloon:
Browse files

#1064056 Sorting implementation for Attributes

parent 1f7a6b5e
No related branches found
No related tags found
No related merge requests found
Showing
with 295 additions and 99 deletions
......@@ -180,10 +180,6 @@
<name>ChildType_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>ParentIdPreset_param</name>
<expose v="false" />
</entityParameter>
</children>
</entityProvider>
<entityField>
......@@ -218,6 +214,18 @@
<name>ParentId_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>AttributeCount_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>IncludeParentRecord_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>ObjectType_param</name>
<expose v="false" />
</entityParameter>
</children>
</entityProvider>
<entityConsumer>
......@@ -367,10 +375,6 @@
<name>ParentId_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>ParentIdPreset_param</name>
<expose v="false" />
</entityParameter>
</children>
</entityProvider>
<entityParameter>
......@@ -379,14 +383,89 @@
<description>parent id, this is used for filtering by the parent in the content process</description>
</entityParameter>
<entityParameter>
<name>ParentIdPreset_param</name>
<name>IncludeParentRecord_param</name>
<expose v="true" />
<documentation>%aditoprj%/entity/Attribute_entity/entityfields/includeparentrecord_param/documentation.adoc</documentation>
</entityParameter>
<entityProvider>
<name>AttributeChildren</name>
<sortingField>SORTING</sortingField>
<titlePlural>Child Attributes</titlePlural>
<dependencies>
<entityDependency>
<name>80023321-1954-483f-a4be-b7207557c068</name>
<entityName>Attribute_entity</entityName>
<fieldName>ChildAttributes</fieldName>
<isConsumer v="false" />
</entityDependency>
</dependencies>
<children>
<entityParameter>
<name>ChildId_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>AttributeCount_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>ChildType_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>FilteredAttributeIds_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>IncludeParentRecord_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>ObjectType_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>ThemeObjectRowId_param</name>
<expose v="false" />
</entityParameter>
<entityParameter>
<name>GetOnlyFirstLevelChildren_param</name>
<valueProcess>%aditoprj%/entity/Attribute_entity/entityfields/attributechildren/children/getonlyfirstlevelchildren_param/valueProcess.js</valueProcess>
<expose v="false" />
</entityParameter>
</children>
</entityProvider>
<entityConsumer>
<name>ChildAttributes</name>
<refreshParent v="true" />
<stateProcess>%aditoprj%/entity/Attribute_entity/entityfields/childattributes/stateProcess.js</stateProcess>
<dependency>
<name>dependency</name>
<entityName>Attribute_entity</entityName>
<fieldName>AttributeChildren</fieldName>
</dependency>
<children>
<entityParameter>
<name>DisplaySimpleName_param</name>
<valueProcess>%aditoprj%/entity/Attribute_entity/entityfields/childattributes/children/displaysimplename_param/valueProcess.js</valueProcess>
</entityParameter>
<entityParameter>
<name>ParentId_param</name>
<valueProcess>%aditoprj%/entity/Attribute_entity/entityfields/childattributes/children/parentid_param/valueProcess.js</valueProcess>
</entityParameter>
<entityParameter>
<name>ParentType_param</name>
<valueProcess>%aditoprj%/entity/Attribute_entity/entityfields/childattributes/children/parenttype_param/valueProcess.js</valueProcess>
</entityParameter>
</children>
</entityConsumer>
<entityParameter>
<name>GetOnlyFirstLevelChildren_param</name>
<expose v="true" />
<description>parent id that is used to preset the parent when the action newChildAttribute is used</description>
</entityParameter>
<entityParameter>
<name>IncludeParentRecord_param</name>
<name>ParentType_param</name>
<expose v="true" />
<documentation>%aditoprj%/entity/Attribute_entity/entityfields/includeparentrecord_param/documentation.adoc</documentation>
</entityParameter>
</entityFields>
<recordContainers>
......
......@@ -2,5 +2,5 @@ import("system.neon");
import("system.result");
import("system.vars");
if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW && vars.get("$param.ParentIdPreset_param") && vars.get("$this.value") == null)
result.string(vars.get("$param.ParentIdPreset_param"));
\ No newline at end of file
if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW && vars.get("$param.ParentId_param") && vars.get("$this.value") == null)
result.string(vars.get("$param.ParentId_param"));
\ No newline at end of file
......@@ -11,7 +11,6 @@ var state = neon.COMPONENTSTATE_EDITABLE;
if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_EDIT || vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW)
{
var type = vars.get("$field.ATTRIBUTE_TYPE");
var parentType = AttributeUtil.getAttributeType(vars.get("$field.ATTRIBUTE_PARENT_ID"));
if (AttributeTypeUtil.isGroupType(type))
{
var hasSubordinate = newSelect("count(*)")
......@@ -24,4 +23,4 @@ if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_EDIT || vars.get("$sys.r
else if (AttributeUtil.hasRelations(vars.get("$field.UID")))
state = neon.COMPONENTSTATE_READONLY;
}
result.string(state)
\ No newline at end of file
result.string(state);
\ No newline at end of file
......@@ -4,9 +4,9 @@ import("system.result");
import("system.vars");
import("Attribute_lib");
if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW && vars.get("$field.ATTRIBUTE_PARENT_ID"))
if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW && (vars.get("$param.ParentType_param") || vars.get("$field.ATTRIBUTE_PARENT_ID")))
{
var parentType = AttributeUtil.getAttributeType(vars.get("$field.ATTRIBUTE_PARENT_ID"));
var parentType = vars.get("$param.ParentType_param") || AttributeUtil.getAttributeType(vars.get("$field.ATTRIBUTE_PARENT_ID"));
var type = vars.get("$this.value");
var possibleTypes = AttributeTypeUtil.getPossibleChildren(parentType);
......
......@@ -8,8 +8,8 @@ if (vars.exists("$local.rows"))
var row = vars.get("$local.rows");
var type = row[0].ATTRIBUTE_TYPE.trim();
if (AttributeTypeUtil.isGroupType(type))
params["ParentIdPreset_param"] = row[0].UID;
params["ParentId_param"] = row[0].UID;
else if (row[0].ATTRIBUTE_PARENT_ID)
params["ParentIdPreset_param"] = row[0].ATTRIBUTE_PARENT_ID;
params["ParentId_param"] = row[0].ATTRIBUTE_PARENT_ID;
}
neon.openContext("Attribute", null, null, neon.OPERATINGSTATE_NEW, params);
\ No newline at end of file
import("system.result");
result.string(true);
\ No newline at end of file
import("system.result");
result.string(true);
\ No newline at end of file
import("system.vars");
import("system.result");
result.string(vars.get("$field.UID"));
\ No newline at end of file
import("system.result");
import("system.vars");
result.string(vars.get("$field.ATTRIBUTE_TYPE"));
\ No newline at end of file
import("system.result");
import("system.neon");
import("system.vars");
import("Attribute_lib");
result.string(AttributeTypeUtil.isGroupType(vars.get("$field.ATTRIBUTE_TYPE")) ? neon.COMPONENTSTATE_EDITABLE : neon.COMPONENTSTATE_INVISIBLE);
\ No newline at end of file
......@@ -7,7 +7,9 @@ import("Attribute_lib");
if (vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW || vars.get("$sys.recordstate") == neon.OPERATINGSTATE_EDIT)
{
var type;
if (vars.get("$field.ATTRIBUTE_PARENT_ID"))
if (vars.get("$param.ParentType_param"))
type = vars.get("$param.ParentType_param");
else if (vars.get("$field.ATTRIBUTE_PARENT_ID"))
type = AttributeUtil.getAttributeType(vars.get("$field.ATTRIBUTE_PARENT_ID"));
else
type = $AttributeTypes.GROUP.toString(); //GROUP can have everything except COMBOVALUE as child
......
......@@ -14,98 +14,90 @@ var childId = vars.get("$param.ChildId_param");
var childType = vars.get("$param.ChildType_param");
var objectType = vars.get("$param.ObjectType_param");
var filteredIds = vars.getString("$param.FilteredAttributeIds_param") ? JSON.parse(vars.getString("$param.FilteredAttributeIds_param")) : null
var filteredIds = vars.get("$param.FilteredAttributeIds_param") ? JSON.parse(vars.getString("$param.FilteredAttributeIds_param")) : null
var attributeCountObj = vars.get("$param.AttributeCount_param") ? JSON.parse(vars.getString("$param.AttributeCount_param")) : null;
var displaySimpleName = vars.getString("$param.DisplaySimpleName_param") == "true" ? true : false;
var displaySimpleName = Utils.toBoolean(vars.get("$param.DisplaySimpleName_param"));
var themeObjectRowId = vars.get("$param.ThemeObjectRowId_param");
var parentId = vars.get("$param.ParentId_param");
var includeParentRecord = vars.get("$param.IncludeParentRecord_param");
var includeParentRecord = Utils.toBoolean(vars.get("$param.IncludeParentRecord_param"));
var onlyFirstLevelChildren = Utils.toBoolean(vars.get("$param.GetOnlyFirstLevelChildren_param"));
var fetchUsages = false;
var translateName = false;
var condition = newWhere();
var emptyResult = function ()
if (vars.exists("$local.idvalues") && vars.get("$local.idvalues"))
{
if (vars.exists("$local.idvalues") && vars.get("$local.idvalues"))
{
condition.andIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", vars.get("$local.idvalues"), SqlBuilder.IN());
fetchUsages = true;
return false;
}
if (childId) //if a childId is given, it is the lookup for selecting the superordinate attribute
{
condition.and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypeUtil.getGroupTypes(childType), SqlBuilder.IN());
//filter out the child and all children of the child, because an attribute can't have itself or a subordinate attribute as parent
condition.andIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", [childId].concat(AttributeUtil.getAllChildren(childId)), SqlBuilder.NOT_IN());
}
else if (objectType) //if there's an objectType, it comes from the AttributeRelation entity (lookup for the attribute selection)
{
translateName = true;
var ids = AttributeUtil.getPossibleAttributes(objectType, true, filteredIds, attributeCountObj);
if (ids.length === 0)
return true;
condition.and("AB_ATTRIBUTE.AB_ATTRIBUTEID", ids, SqlBuilder.IN());
condition.and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.THEME, !themeObjectRowId ? SqlBuilder.NOT_EQUAL() : undefined);
}
else if (parentId)
condition.andIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", vars.get("$local.idvalues"), SqlBuilder.IN());
fetchUsages = true;
}
else if (childId) //if a childId is given, it is the lookup for selecting the superordinate attribute
{
condition.and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", AttributeTypeUtil.getGroupTypes(childType), SqlBuilder.IN());
//filter out the child and all children of the child, because an attribute can't have itself or a subordinate attribute as parent
condition.andIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", [childId].concat(AttributeUtil.getAllChildren(childId)), SqlBuilder.NOT_IN());
}
else if (objectType) //if there's an objectType, it comes from the AttributeRelation entity (lookup for the attribute selection)
{
translateName = true;
var ids = AttributeUtil.getPossibleAttributes(objectType, true, filteredIds, attributeCountObj);
if (Utils.isEmpty(ids))
condition.noResult();
condition.and("AB_ATTRIBUTE.AB_ATTRIBUTEID", ids, SqlBuilder.IN());
condition.and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.THEME, !themeObjectRowId ? SqlBuilder.NOT_EQUAL() : undefined);
}
else if (parentId)
{
if (onlyFirstLevelChildren)
condition.and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", parentId);
else
{
condition.and("AB_ATTRIBUTE.AB_ATTRIBUTEID", AttributeUtil.getAllChildren(parentId), SqlBuilder.IN());
translateName = true;
if(includeParentRecord == "true")
if(includeParentRecord)
condition.or("AB_ATTRIBUTE.AB_ATTRIBUTEID", parentId);
}
else
{
fetchUsages = true;
}
}
else
{
fetchUsages = true;
}
//when there are filters selected, add them to the conditon
if (vars.exists("$local.filter") && vars.get("$local.filter"))
{
var filter = vars.get("$local.filter");
if (filter.filter)
condition.andIfSet(JditoFilterUtils.getSqlCondition(filter.filter, "AB_ATTRIBUTE", undefined, {
// special filter for usage
USAGE_FILTER : function (pValue, pOperator)
{
var cond = newWhere();
var subSelect = newSelect("1").from("AB_ATTRIBUTEUSAGE", "attrUse").where("attrUse.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID");
switch (pOperator)
{
case "EQUAL":
case "NOT_EQUAL":
subSelect.and(["AB_ATTRIBUTEUSAGE", "AB_ATTRIBUTE_ID", "attrUse"], pValue);
case "ISNULL":
case "ISNOTNULL":
return cond.and(null, subSelect, pOperator == "NOT_EQUAL" || pOperator == "ISNULL" ? SqlBuilder.NOT_EXISTS() : SqlBuilder.EXISTS());
}
return cond;
}
}));
}
return false;
}();
var filterCondition = new FilterSqlTranslator(vars.get("$local.filter"), "AB_ATTRIBUTE")
.addSpecialFieldConditionFn("USAGE_FILTER", function (pValue, pOperator)
{
var cond = newWhere();
var subSelect = newSelect("1").from("AB_ATTRIBUTEUSAGE", "attrUse").where("attrUse.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID");
switch (pOperator)
{
case "EQUAL":
case "NOT_EQUAL":
subSelect.and(["AB_ATTRIBUTEUSAGE", "AB_ATTRIBUTE_ID", "attrUse"], pValue);
case "ISNULL":
case "ISNOTNULL":
return cond.and(null, subSelect, pOperator == "NOT_EQUAL" || pOperator == "ISNULL" ? SqlBuilder.NOT_EXISTS() : SqlBuilder.EXISTS());
}
return cond;
})
.getSqlCondition();
condition.andIfSet(filterCondition);
var usages;
if (fetchUsages) //this query is only necessary in Attribute, not in AttributeRelation
{
var usageTbl = newSelect("AB_ATTRIBUTE_ID, OBJECT_TYPE")
.from("AB_ATTRIBUTEUSAGE")
.join("AB_ATTRIBUTE", newWhere("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID"))
.whereIfSet(condition)
.table();
.from("AB_ATTRIBUTEUSAGE")
.join("AB_ATTRIBUTE", "AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
.whereIfSet(condition)
.table();
usages = {};
for (let i = 0, l = usageTbl.length; i < l; i++)
{
......@@ -117,16 +109,18 @@ if (fetchUsages) //this query is only necessary in Attribute, not in AttributeRe
}
}
var attributes = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_PARENT_ID, ATTRIBUTE_NAME, ATTRIBUTE_ACTIVE, DROPDOWNDEFINITION, DROPDOWNFILTER, SORTING, ATTRIBUTE_TYPE, "
+ KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.attributeType(), "ATTRIBUTE_TYPE") //3
+ ", '', '', ''")
var attributes = newSelect(["AB_ATTRIBUTEID, ATTRIBUTE_PARENT_ID, ATTRIBUTE_NAME, ATTRIBUTE_ACTIVE, DROPDOWNDEFINITION, DROPDOWNFILTER, SORTING, ATTRIBUTE_TYPE",
KeywordUtils.getResolvedTitleSqlPart($KeywordRegistry.attributeType(), "ATTRIBUTE_TYPE"), //3
"'', '', ''"])
.from("AB_ATTRIBUTE")
.whereIfSet(condition)
.orderBy("ATTRIBUTE_PARENT_ID asc, SORTING asc")
.table(emptyResult);
.orderBy("ATTRIBUTE_PARENT_ID, SORTING, ATTRIBUTE_NAME")
.table();
//TODO: attribute name caching like keywords
var allNames = !emptyResult ? newSelect("AB_ATTRIBUTEID, ATTRIBUTE_PARENT_ID, ATTRIBUTE_NAME").from("AB_ATTRIBUTE").table() : [];
var allNames = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_PARENT_ID, ATTRIBUTE_NAME")
.from("AB_ATTRIBUTE")
.table(Utils.isEmpty(attributes));
var attrNameData = {};
for (let i = 0, l = allNames.length; i < l; i++)
{
......
import("system.vars");
import("Sql_lib");
import("Attribute_lib");
var rowdata = vars.get("$local.rowdata");
......@@ -12,4 +13,22 @@ new SqlBuilder().insertFields({
"ATTRIBUTE_TYPE" : rowdata["ATTRIBUTE_TYPE.value"],
"DROPDOWNFILTER" : rowdata["DROPDOWNFILTER.value"],
"SORTING" : rowdata["SORTING.value"]
}, "AB_ATTRIBUTE");
\ No newline at end of file
}, "AB_ATTRIBUTE");
if (rowdata["ATTRIBUTE_PARENT_ID.value"] && rowdata["ATTRIBUTE_TYPE.value"] !== $AttributeTypes.COMBOVALUE.toString() && vars.get("$param.GetOnlyFirstLevelChildren_param"))
{
var parentUsages = newSelect("OBJECT_TYPE")
.from("AB_ATTRIBUTEUSAGE")
.where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", rowdata["ATTRIBUTE_PARENT_ID.value"])
.arrayColumn();
var usageValues = {};
if (AttributeTypeUtil.isSingleSelection(rowdata["ATTRIBUTE_TYPE.value"]))
usageValues["MAX_COUNT"] = 1;
parentUsages.forEach(function (usageContext)
{
usageValues["OBJECT_TYPE"] = usageContext;
new SqlBuilder().insertFields(usageValues, "AB_ATTRIBUTEUSAGE", "AB_ATTRIBUTEUSAGEID");
});
}
\ No newline at end of file
import("Entity_lib");
import("system.util");
import("system.result");
import("system.neon");
import("system.vars");
if(vars.get("$sys.recordstate") == neon.OPERATINGSTATE_NEW)
result.string(vars.get("$sys.date"));
\ No newline at end of file
result.string(vars.get("$sys.date"));
"$field.Attributes.deletedRows";
EntityConsumerRowsHelper.getCurrentConsumerRows("Attributes", ["VALUE"])
\ No newline at end of file
......@@ -30,5 +30,13 @@
<name>a380915a-6946-4923-9b13-7a981606ce60</name>
<view>AttributeLookup_view</view>
</neonViewReference>
<neonViewReference>
<name>db1f22e8-46f8-40aa-a5eb-00c606666c96</name>
<view>AttributeMultiEdit_view</view>
</neonViewReference>
<neonViewReference>
<name>1cf7d11d-d593-4518-b7aa-aca1a9a2fb8a</name>
<view>AttributeList_view</view>
</neonViewReference>
</references>
</neonContext>
......@@ -50,5 +50,10 @@
<entityField>AttributeUsages</entityField>
<view>AttributeUsageMultiEdit_view</view>
</neonViewReference>
<neonViewReference>
<name>90d4edc1-699d-413f-bbcd-d14a90cd8cf9</name>
<entityField>ChildAttributes</entityField>
<view>AttributeMultiEdit_view</view>
</neonViewReference>
</children>
</neonView>
<?xml version="1.0" encoding="UTF-8"?>
<neonView xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.6" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonView/1.1.6">
<name>AttributeList_view</name>
<majorModelMode>DISTRIBUTED</majorModelMode>
<layout>
<noneLayout>
<name>layout</name>
</noneLayout>
</layout>
<children>
<titledListViewTemplate>
<name>AttributeList</name>
<entityField>#ENTITY</entityField>
<columns>
<neonTitledListTableColumn>
<name>96544713-a302-4e2f-ab7f-6c02d44d9908</name>
<entityField>ATTRIBUTE_NAME</entityField>
</neonTitledListTableColumn>
<neonTitledListTableColumn>
<name>5c536673-78f5-482c-aa98-f027f08659e1</name>
<entityField>ATTRIBUTE_TYPE</entityField>
</neonTitledListTableColumn>
<neonTitledListTableColumn>
<name>fcd31169-8b19-4165-bb85-200ab6045cdd</name>
<entityField>DROPDOWNDEFINITION</entityField>
</neonTitledListTableColumn>
</columns>
</titledListViewTemplate>
</children>
</neonView>
<?xml version="1.0" encoding="UTF-8"?>
<neonView xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.1.6" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/neonView/1.1.6">
<name>AttributeMultiEdit_view</name>
<majorModelMode>DISTRIBUTED</majorModelMode>
<layout>
<noneLayout>
<name>layout</name>
</noneLayout>
</layout>
<children>
<genericMultipleViewTemplate>
<name>GenericMultiple</name>
<entityField>#ENTITY</entityField>
<columns>
<neonGenericMultipleTableColumn>
<name>59d4b058-675b-4124-a510-a576e2222815</name>
<entityField>ATTRIBUTE_NAME</entityField>
</neonGenericMultipleTableColumn>
<neonGenericMultipleTableColumn>
<name>e6660015-3e4a-47d3-aee5-0458e8e8b8e3</name>
<entityField>ATTRIBUTE_TYPE</entityField>
</neonGenericMultipleTableColumn>
<neonGenericMultipleTableColumn>
<name>67d03576-b62e-42ab-a17e-220b2468a315</name>
<entityField>DROPDOWNDEFINITION</entityField>
</neonGenericMultipleTableColumn>
</columns>
</genericMultipleViewTemplate>
</children>
</neonView>
......@@ -45,5 +45,10 @@
<entityField>AttributeUsages</entityField>
<view>AttributeUsageList_view</view>
</neonViewReference>
<neonViewReference>
<name>87719160-f477-49c7-b8de-5386d4c03a45</name>
<entityField>ChildAttributes</entityField>
<view>AttributeList_view</view>
</neonViewReference>
</children>
</neonView>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment