Issue #1167 : Implement group merge function

* Implement new API function pwg.groups.merge
 * Generalize the delete group function in admin/include/functions.php
 * Implement merge action in Group Manager
 * Add loading state on grouped delete in group manager
 * Member manager's design fixes
This commit is contained in:
Zacharie
2020-05-13 16:57:52 +02:00
committed by plegall
parent 1d3c08616b
commit df46475e1b
8 changed files with 402 additions and 144 deletions
+44
View File
@@ -2454,6 +2454,50 @@ SELECT name
return $groupname;
}
function delete_groups($group_ids) {
$group_id_string = implode(',', $group_ids);
// destruction of the access linked to the group
$query = '
DELETE
FROM '. GROUP_ACCESS_TABLE .'
WHERE group_id IN('. $group_id_string .')
;';
pwg_query($query);
// destruction of the users links for this group
$query = '
DELETE
FROM '. USER_GROUP_TABLE .'
WHERE group_id IN('. $group_id_string .')
;';
pwg_query($query);
$query = '
SELECT id, name
FROM `'. GROUPS_TABLE .'`
WHERE id IN('. $group_id_string .')
;';
$group_list = query2array($query, 'id', 'name');
$groupids = array_keys($group_list);
// destruction of the group
$query = '
DELETE
FROM `'. GROUPS_TABLE .'`
WHERE id IN('. $group_id_string .')
;';
pwg_query($query);
trigger_notify('delete_group', $groupids);
pwg_activity('group', $groupids, 'delete');
return $group_list;
}
/**
* Returns the username corresponding to the given user identifier if exists.
*
+1 -1
View File
@@ -602,7 +602,7 @@ input:focus + .slider {
border:1px solid #3c3c3c;
}
.ValidationUserAssociated{
.ValidationUserAssociated, .ValidationUserDissociated{
color:#00aa00;
background-color:#c2f5c2;
}
+197 -88
View File
@@ -90,6 +90,7 @@ jQuery(document).ready(function () {
newgroup.css("order", -id);
newgroup.attr("data-id", id);
newgroup.find("#group_name").html(name);
newgroup.find(".group_name-editable").val(name);
newgroup.find(".Group-checkbox label").attr("for", "Group-Checkbox-selection-" + id);
newgroup.find(".Group-checkbox input").attr("id", "Group-Checkbox-selection-" + id);
newgroup.find(".input-edit-group-name").attr("placeholder", name);
@@ -109,9 +110,9 @@ jQuery(document).ready(function () {
newgroup.find(".groupMessage").fadeIn();
newgroup.find(".groupMessage").delay(DELAY_FEEDBACK).fadeOut();
} else {
$("#showAddGroup .groupError").html(str_name_taken);
$("#showAddGroup .groupError").fadeIn();
$("#showAddGroup .groupError").delay(DELAY_FEEDBACK).fadeOut();
$("#addGroupForm .groupError").html(str_name_taken);
$("#addGroupForm .groupError").fadeIn();
$("#addGroupForm .groupError").delay(DELAY_FEEDBACK).fadeOut();
}
},
error: function (err) {
@@ -129,54 +130,14 @@ jQuery(document).ready(function () {
setupGroupBox($(this));
});
});
var setupGroupBox = function (groupBox) {
var id = groupBox.data("id");
/* Change background color of group block if checked in selection mode */
groupBox.find(".Group-checkbox input[type='checkbox']").change(function () {
if ($(this).is(":checked")) {
groupBox.addClass("OrangeBackground");
groupBox.find(".icon-users-1").addClass("OrangeIcon");
groupBox.find(".group_number_users").addClass("OrangeFont");
//Display item selection on selection panel
item = $(
"<div data-id=" +
groupBox.data("id") +
">" +
"<a class='icon-cancel'></a>" +
"<p>" +
groupBox.find("#group_name").html() +
"</p> </div>"
);
item.appendTo(".DeleteGroupList");
item.find("a").on("click", function () {
groupBox.find(".Group-checkbox input").attr("checked", false);
groupBox
.find(".Group-checkbox input[type='checkbox']")
.trigger("change");
});
updateSelectionPanel();
option = $('<option value="'+id+'">'+groupBox.find("#group_name").html()+'</option>')
option.appendTo("#MergeOptionsChoices");
} else {
groupBox.removeClass("OrangeBackground");
groupBox.find(".icon-users-1").removeClass("OrangeIcon");
groupBox.find(".group_number_users").removeClass("OrangeFont");
$(".DeleteGroupList div").each(function () {
if ($(this).data("id") == id) {
$(this).remove();
}
});
updateSelectionPanel();
$("#MergeOptionsChoices option").each(function () {
if ($(this).attr("value") == id) {
$(this).remove();
}
});
}
toogleSelection(id, groupBox.find(".Group-checkbox input[type='checkbox']").is(":checked"));
});
groupBox.find(".Group-checkbox input[type='checkbox']").attr("checked", false)
/* Display the option on the click on "..." */
groupBox.find(".group-dropdown-options").click(function GroupOptions() {
@@ -186,18 +147,40 @@ var setupGroupBox = function (groupBox) {
/* Set the delete action */
groupBox.find("#GroupDelete").on("click", function () {
$.confirm({
title: str_delete+' '+groupBox.find("#group_name").html(),
content: str_are_you_sure,
title: str_delete.replace("%s",groupBox.find("#group_name").html()),
draggable: false,
titleClass: "groupDeleteConfirm",
theme: "modern",
content: "",
animation: "zoom",
boxWidth: '30%',
useBootstrap: false,
type: 'red',
typeAnimated: true,
animateFromElement: false,
backgroundDismiss: true,
typeAnimated: false,
buttons: {
confirm: {
text: str_yes_delete_confirmation,
btnClass: 'btn-red',
action: function () {
deleteGroup(id);
let groupName = groupBox.find(".Group-name-container p").html();
deleteGroup(id).then(() => {
$.alert({
title: str_group_deleted.replace("%s",groupName),
icon: 'icon-ok',
titleClass: "groupDeleteAlert",
theme:"modern",
closeIcon: true,
content: "",
animation: "zoom",
boxWidth: '20%',
useBootstrap: false,
backgroundDismiss: true,
animateFromElement: false,
typeAnimated: false,
});
});
}
},
cancel: {
@@ -249,28 +232,71 @@ var setupGroupBox = function (groupBox) {
};
var toogleSelection = function(group_id, toggle) {
groupBox = $("#group-"+group_id);
if (toggle) {
groupBox.find(".Group-checkbox input").attr("checked", true);
groupBox.addClass("OrangeBackground");
groupBox.find(".icon-users-1").addClass("OrangeIcon");
groupBox.find(".group_number_users").addClass("OrangeFont");
//Display item selection on selection panel
item = $(
"<div data-id=" +
group_id +
">" +
"<a class='icon-cancel'></a>" +
"<p>" +
groupBox.find("#group_name").html() +
"</p> </div>"
);
item.appendTo(".DeleteGroupList");
item.find("a").on("click", function () {
groupBox.find(".Group-checkbox input").attr("checked", false);
toogleSelection(group_id);
});
updateSelectionPanel();
option = $('<option value="'+group_id+'">'+groupBox.find("#group_name").html()+'</option>')
option.appendTo("#MergeOptionsChoices");
} else {
groupBox.find(".Group-checkbox input").attr("checked", false);
groupBox.removeClass("OrangeBackground");
groupBox.find(".icon-users-1").removeClass("OrangeIcon");
groupBox.find(".group_number_users").removeClass("OrangeFont");
$(".DeleteGroupList div").each(function () {
if ($(this).data("id") == group_id) {
$(this).remove();
}
});
updateSelectionPanel();
$("#MergeOptionsChoices option").each(function () {
if ($(this).attr("value") == group_id) {
$(this).remove();
}
});
}
}
/* Group Ajax and Display Functions */
var deleteGroup = function (id) {
jQuery.ajax({
url: "ws.php?format=json&method=pwg.groups.delete",
type: "POST",
data: "group_id=" + id + "&pwg_token=" + pwg_token,
success: function (raw_data) {
data = jQuery.parseJSON(raw_data);
if (data.stat === "ok") {
$.alert({
title: str_group_deleted,
content: "",
boxWidth: '20%',
useBootstrap: false
});
$("#group-" + id).remove();
}
},
error: function (err) {
console.log(err);
},
});
return new Promise((resolve, reject) => {
jQuery.ajax({
url: "ws.php?format=json&method=pwg.groups.delete",
type: "POST",
data: "group_id=" + id + "&pwg_token=" + pwg_token,
success: function (raw_data) {
data = jQuery.parseJSON(raw_data);
if (data.stat === "ok") {
$("#group-" + id).remove();
$(".DeleteGroupList div[data-id="+id+"]").remove()
resolve();
}
},
error: function (err) {
console.log(err);
},
});
})
};
var renameGroup = function(id, newName) {
@@ -338,13 +364,9 @@ var setDefaultGroup = function (id, is_default) {
if (data.stat === "ok") {
if (is_default) {
setupDefaultActions(id,true)
//$("#group-" + id).find(".groupMessage").html(str_set_default_state);
} else {
setupDefaultActions(id,false)
//$("#group-" + id).find(".groupMessage").html(str_unset_default_state);
}
//$("#group-" + id).find(".groupMessage").fadeIn();
//$("#group-" + id).find(".groupMessage").delay(DELAY_FEEDBACK).fadeOut();
}
},
error: function (err) {
@@ -379,6 +401,7 @@ var setupDefaultActions = function(id, is_default) {
-------*/
$(function () {
$("#toggleSelectionMode").attr("checked", false)
$("#toggleSelectionMode").click(function () {
if ($(this).is(":checked")) {
$(".in-selection-mode").show();
@@ -478,17 +501,98 @@ var buttonUnavailable = function(button) {
button.removeAttr("OnClick");
}
/*-------
Merge function on button's pannel
-------*/
$('.ConfirmMergeButton').on("click", function() {
let loadState = new TemporaryState();
loadState.changeAttribute($('.ConfirmMergeButton'), "style", "pointer-events: none");
loadState.changeHTML($('.ConfirmMergeButton'), "<i class='icon-spin6 animate-spin'> </i>");
loadState.removeClass($('.ConfirmMergeButton'), "icon-ok");
merge_group = [];
str_merge_group = "";
dest_grp = $("#MergeOptionsChoices").val();
$(".DeleteGroupList div").each(function () {
if (dest_grp != $(this).attr("data-id")) {
str_merge_group += "&merge_group_id[]="+$(this).attr("data-id");
merge_group.push($(this).attr("data-id"));
}
})
jQuery.ajax({
url: "ws.php?format=json&method=pwg.groups.merge",
type: "POST",
data: "destination_group_id=" + dest_grp + str_merge_group + "&pwg_token=" + pwg_token,
success: function (raw_data) {
loadState.reverse();
data = jQuery.parseJSON(raw_data);
if (data.stat === "ok") {
updateSelectionPanel('Selection');
console.log(data);
merge_group.forEach(function(id) {
($("#group-"+id).fadeOut(complete=function(){
$(this).remove();
}))
})
toogleSelection(dest_grp, false)
$("#group-"+dest_grp + " .group_number_users").html("<i class='icon-spin6 animate-spin'> </i>");
jQuery.ajax({
url: "ws.php?format=json&method=pwg.users.getList",
type: "POST",
data: "group_id=" + dest_grp,
success: function (raw_data) {
data = jQuery.parseJSON(raw_data);
let number = data.result.users.length;
$("#group-"+dest_grp + " .group_number_users").html(
number + " " + ((number > 1)? str_members_default:str_member_default)
);
}
})
};
}
})
})
/*-------
Delete function on button's pannel
-------*/
$('.ConfirmDeleteButton').on("click", function() {
let names = [];
let promises = [];
$('.DeleteGroupList div').each(function () {
deleteGroup($(this).data('id'));
$(this).remove();
let id = $(this).data('id');
names.push($("#group-"+id+" #group_name").html());
promises.push(deleteGroup(id));
});
let loadState = new TemporaryState;
loadState.changeAttribute($('.ConfirmDeleteButton'), "style", "pointer-events: none");
loadState.changeHTML($('.ConfirmDeleteButton'), "<i class='icon-spin6 animate-spin'> </i>");
loadState.removeClass($('.ConfirmDeleteButton'),"icon-ok");
Promise.all(promises).then(() => {
loadState.reverse();
updateSelectionPanel("NoSelection");
$.alert({
title: str_groups_deleted.replace("%s",names.toString()),
titleClass: "groupDeleteAlert",
theme: "modern",
icon: 'icon-ok',
closeIcon: true,
content: "",
animation: "zoom",
boxWidth: '20%',
useBootstrap: false,
backgroundDismiss: true,
animateFromElement: false,
typeAnimated: false,
backgroundDismiss: true,
});
})
})
/*-------
Manage User Part
@@ -505,9 +609,13 @@ var usersInGroup = [];
// Max offset of the user container (322 = 6 lines)
var maxOffsetUserCont = 322;
var infoUsers = $("<div class='ValidationUserAssociated'>"
var dissociateUserInfo = $("<div class='ValidationUserDissociated'>"
+ "<p class='icon-ok'></p>"
+ "</div>")
+ "</div>").appendTo(".group-name-block").hide();
var associateUserInfo = $("<div class='ValidationUserAssociated'>"
+ "<p class='icon-ok'></p>"
+ "</div>");
// Setup the user research bar
$(function() {
@@ -625,10 +733,10 @@ var getUserDisplay = function(username, user_id, grp_id) {
success: function (raw_data) {
data = jQuery.parseJSON(raw_data);
if (data.stat === "ok") {
infoUsers.remove();
infoUsers.insertAfter(userBlock).hide();
infoUsers.find("p").html(str_user_dissociated);
infoUsers.fadeIn()
let str = str_user_dissociated.replace("%s", username)
dissociateUserInfo.find("p").html(str);
dissociateUserInfo.fadeIn()
dissociateUserInfo.delay(DELAY_FEEDBACK).fadeOut()
userBlock.remove()
@@ -652,7 +760,7 @@ var getUserDisplay = function(username, user_id, grp_id) {
//Update member number function
function updateMembernumber(number, grp_id) {
$(".GroupContainer[data-id="+grp_id+"] .group_number_users")
.html(number + " " + str_member_default);
.html(number + " " + ((number > 1)? str_members_default:str_member_default));
$(".UserNumberBadge").html(number);
$(".AmountOfUsersShown strong:nth-child(2)").html(number)
$(".AmountOfUsersShown strong:nth-child(1)").html($(".UsernameBlock").length)
@@ -691,10 +799,11 @@ $(".AddUserBlock button").on("click", function () {
})
let userBlock = getUserDisplay(username, id, grp_id).prependTo(".UsersInGroupList");
infoUsers.remove();
infoUsers.insertAfter(userBlock).hide();
infoUsers.find("p").html(str_user_associated);
infoUsers.fadeIn()
associateUserInfo.remove()
associateUserInfo.insertAfter(userBlock);
associateUserInfo.find("p").html(str_user_associated);
associateUserInfo.fadeIn()
associateUserInfo.delay(DELAY_FEEDBACK).fadeOut()
updateUserSearch();
@@ -716,7 +825,6 @@ $(".input-user-name").on("input", function() {
searchString = $(this).val().toLowerCase();
grp_id = $(".UserListPopIn").data("group_id");
if (searchString != "") {
infoUsers.remove();
$(".UsersInGroupListContainer").css("min-height", $(".UsersInGroupListContainer").height())
usersInGroup.forEach(function(u) {
let isSearched = u.username.toLowerCase().includes(searchString)
@@ -739,6 +847,7 @@ $(".input-user-name").on("input", function() {
i++;
}
}
$(".AmountOfUsersShown strong:nth-child(1)").html($(".UsernameBlock").length)
while ($(".UsersInGroupList").height() > maxOffsetUserCont) {
$(".UsernameBlock").last().remove();
}
+12 -14
View File
@@ -1,20 +1,19 @@
{footer_script}
var pwg_token = "{$PWG_TOKEN}";
var str_member_default = "{'member'|@translate}"
var str_members_default = "{'members'|@translate}"
var str_group_created = "{'Group added'|@translate}"
var str_renaming_done = "{'Group renamed'|@translate}"
var str_name_taken = "{'Name is already taken'|@translate}"
var str_group_deleted = "{'Group succesfully deleted'|@translate}"
var str_group_deleted = '{'Group "%s" succesfully deleted'|@translate}'
var str_groups_deleted = '{'Groups \{%s\} succesfully deleted'|@translate}'
var str_set_default = "{'Set as group for new users'|@translate}"
var str_unset_default = "{'Unset as group for new users'|@translate}"
var str_set_default_state = "{'Group for new users set'|@translate}"
var str_unset_default_state = "{'Group for new users unset'|@translate}"
var str_delete = "{'Delete'|@translate}"
var str_are_you_sure = "{'Are you sure ?'|@translate}"
var str_yes_delete_confirmation = "{'Yes, delete it'|@translate}"
var str_no_delete_confirmation = "{"No, I've changed my mind"|@translate}"
var str_user_associated = "{"User Associated"|@translate}"
var str_user_dissociated = "{"User Dissociated"|@translate}"
var str_delete = '{'Delete group "%s"?'|@translate}'
var str_yes_delete_confirmation = "{'Yes, delete'|@translate}"
var str_no_delete_confirmation = "{"No, I have changed my mind"|@translate}"
var str_user_associated = "{"User associated"|@translate}"
var str_user_dissociated = '{'User "%s" Dissociated from this group'|@translate}'
var str_user_list = "{"User List"|@translate}"
var serverKey = '{$CACHE_KEYS.users}'
@@ -122,20 +121,19 @@ var rootUrl = '{$ROOT_URL}'
<p>{'Choose which group to merge these groups into'|@translate}</p>
<p class="ItalicTextInfo">{'The other groups will be removed'|@translate}</p>
<div class="MergeOptionsContainer">
<label for="MergeOptionsChoices">{'The other groups will be removed'|@translate}</label>
<select id="MergeOptionsChoices"> {* TODO *}
<select id="MergeOptionsChoices">
</select>
</div>
<button class="icon-ok ConfirmMergeButton" onclick="updateSelectionPanel('Selection')">Confirm merge</button>
<button class="icon-ok ConfirmMergeButton">Confirm merge</button>
<a id="CancelMerge" onclick="updateSelectionPanel('Selection')">Cancel</a>
</div>
<div id="ConfirmGroupAction">
<p>You are about to delete <span class="number-Selected">0</span> groups, are you sure?</p>
<button class="icon-ok ConfirmDeleteButton" onclick="updateSelectionPanel('Selection')">Yes, delete</button>
<a id="CancelDelete" onclick="updateSelectionPanel('Selection')">No, I have changed my mind</a>
<button class="icon-ok ConfirmDeleteButton">{'Yes, delete'|@translate}</button>
<a id="CancelDelete" onclick="updateSelectionPanel('Selection')">{"No, I have changed my mind"|@translate}</a>
</div>
</div>
+44 -1
View File
@@ -2044,6 +2044,35 @@ input:checked + .slider:before {
font-size: 13px;
}
.groupDeleteConfirm, .groupDeleteAlert {
padding-bottom: 0 !important;
color: #3c3c3c !important;
line-height: 28px !important;
}
.groupDeleteConfirm ~ .jconfirm-content-pane, .groupDeleteAlert ~ .jconfirm-content-pane {
height: 0px !important;
margin: 0px !important;
}
.groupDeleteConfirm ~ .jconfirm-buttons button {
text-transform: none !important;
}
.groupDeleteAlert .jconfirm-icon-c i {
color: #0a0 !important;
background-color:#c2f5c2 !important;
border-radius: 20px;
padding: 5px 3px;
border-radius: 60px;
padding: 2px;
font-size: 45px;
}
.groupDeleteAlert .jconfirm-icon-c {
margin-bottom: 20px !important;
}
/*Group checkbox*/
.Group-checkbox{
@@ -2421,7 +2450,7 @@ input:checked + .slider:before {
display:inline-block;
font-size:12px;
text-align:left;
padding:2px;
padding: 4px 6px;
border-radius:10px;
margin-left:-5px;
margin-right:10px;
@@ -2431,6 +2460,20 @@ input:checked + .slider:before {
margin:0;
}
.ValidationUserDissociated{
display:inline-block;
text-align:left;
padding: 6px;
border-radius: 30px;
margin-left: 8px;
}
.ValidationUserDissociated p{
margin:0 !important;
font-size:12px !important;
font-weight: 500 !important;
}
#UserList .FilterUserBlock .AmountOfUsersShown {
display:inline-block;
+1 -1
View File
@@ -632,7 +632,7 @@ input:focus + .slider {
border:1px solid #f3f3f3;
}
.ValidationUserAssociated{
.ValidationUserAssociated, .ValidationUserDissociated{
background-color:#00aa00;
color:#c2f5c2;
}
+88 -39
View File
@@ -104,46 +104,9 @@ function ws_groups_delete($params, &$service)
return new PwgError(403, 'Invalid security token');
}
$group_id_string = implode(',', $params['group_id']);
// destruction of the access linked to the group
$query = '
DELETE
FROM '. GROUP_ACCESS_TABLE .'
WHERE group_id IN('. $group_id_string .')
;';
pwg_query($query);
// destruction of the users links for this group
$query = '
DELETE
FROM '. USER_GROUP_TABLE .'
WHERE group_id IN('. $group_id_string .')
;';
pwg_query($query);
$query = '
SELECT id, name
FROM `'. GROUPS_TABLE .'`
WHERE id IN('. $group_id_string .')
;';
$group_list = query2array($query, 'id', 'name');
$groupnames = array_values($group_list);
$groupids = array_keys($group_list);
// destruction of the group
$query = '
DELETE
FROM `'. GROUPS_TABLE .'`
WHERE id IN('. $group_id_string .')
;';
pwg_query($query);
trigger_notify('delete_group', $groupids);
pwg_activity('group', $groupids, 'delete');
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$groupnames = array_values(delete_groups($params['group_id']));
invalidate_user_cache();
return new PwgNamedArray($groupnames, 'group_deleted');
@@ -264,6 +227,92 @@ SELECT COUNT(*)
return $service->invoke('pwg.groups.getList', array('group_id' => $params['group_id']));
}
/**
* API method
* Merge groups in one other group
* @param mixed[] $params
* @option int destination_group_id
* @option int[] merge_group_id
*/
function ws_groups_merge($params, &$service) {
if (get_pwg_token() != $params['pwg_token'])
{
return new PwgError(403, 'Invalid security token');
}
$all_groups = $params['merge_group_id'];
array_push($all_groups, $params['destination_group_id']);
$all_groups = array_unique($all_groups);
$merge_group = array_diff($params['merge_group_id'], array($params['destination_group_id']));
$query = '
SELECT COUNT(*)
FROM `'. GROUPS_TABLE .'`
WHERE id in ('.implode(',', $all_groups) .')
;';
list($count) = pwg_db_fetch_row(pwg_query($query));
if ($count != count($all_groups))
{
return new PwgError(WS_ERR_INVALID_PARAM, 'All groups does not exist.');
}
$user_in_merge_groups = array();
$user_in_dest = array();
$user_to_add = array();
$query = '
SELECT DISTINCT(user_id)
FROM `'. USER_GROUP_TABLE .'`
WHERE
group_id in ('.implode(',', $merge_group) .')
;';
$user_in_merge_groups = query2array($query, null, 'user_id');
$query = '
SELECT user_id
FROM `'. USER_GROUP_TABLE .'`
WHERE group_id = '.$params['destination_group_id'].'
;';
$user_in_dest = query2array($query, null, 'user_id');;
$user_to_add = array_diff($user_in_merge_groups, $user_in_dest);
$inserts = array();
foreach ($user_to_add as $user)
{
$inserts[] = array(
'group_id' => $params['destination_group_id'],
'user_id' => $user,
);
}
mass_inserts(
USER_GROUP_TABLE,
array('group_id', 'user_id'),
$inserts,
array('ignore'=>true)
);
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
invalidate_user_cache();
pwg_activity('group', $params['destination_group_id'], 'edit');
foreach ($user_to_add as $user_id)
{
pwg_activity('user', $user_id, 'edit');
}
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
delete_groups($merge_group);
return $service->invoke('pwg.groups.getList', array('group_id' => $params['destination_group_id']));
}
/**
* API method
* Removes user(s) from a group
+15
View File
@@ -910,6 +910,21 @@ function ws_addDefaultMethods( $arr )
array('admin_only'=>true, 'post_only'=>true)
);
$service->addMethod(
'pwg.groups.merge',
'ws_groups_merge',
array(
'destination_group_id' => array('type'=>WS_TYPE_ID,
'info'=>'Destination group (is not necessarily part of groups to merge)'),
'merge_group_id' => array('flags'=>WS_PARAM_FORCE_ARRAY,
'type'=>WS_TYPE_ID),
'pwg_token' => array(),
),
'Merge groups in one other group',
$ws_functions_root . 'pwg.groups.php',
array('admin_only'=>true, 'post_only'=>true)
);
$service->addMethod(
'pwg.users.getList',
'ws_users_getList',