diff --git a/web/client/actions/__tests__/usergroups-test.js b/web/client/actions/__tests__/usergroups-test.js
new file mode 100644
index 0000000000..38f3d82d48
--- /dev/null
+++ b/web/client/actions/__tests__/usergroups-test.js
@@ -0,0 +1,255 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const expect = require('expect');
+const assign = require('object-assign');
+const {
+ GETGROUPS,
+ STATUS_SUCCESS,
+ STATUS_ERROR,
+ getUserGroups,
+ editGroup,
+ EDITGROUP,
+ changeGroupMetadata,
+ EDITGROUPDATA,
+ saveGroup,
+ UPDATEGROUP,
+ deleteGroup,
+ DELETEGROUP,
+ STATUS_DELETED,
+ searchUsers,
+ SEARCHUSERS
+} = require('../usergroups');
+let GeoStoreDAO = require('../../api/GeoStoreDAO');
+let oldAddBaseUri = GeoStoreDAO.addBaseUrl;
+
+describe('Test correctness of the usergroups actions', () => {
+ beforeEach(() => {
+ GeoStoreDAO.addBaseUrl = (options) => {
+ return assign(options, {baseURL: 'base/web/client/test-resources/geostore/'});
+ };
+ });
+
+ afterEach(() => {
+ GeoStoreDAO.addBaseUrl = oldAddBaseUri;
+ });
+ it('get UserGroups', (done) => {
+ const retFun = getUserGroups('usergroups.json', {params: {start: 0, limit: 10}});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ expect(action.type).toBe(GETGROUPS);
+ count++;
+ if (count === 2) {
+ expect(action.status).toBe(STATUS_SUCCESS);
+ expect(action.groups).toExist();
+ expect(action.groups[0]).toExist();
+ expect(action.groups[0].groupName).toExist();
+ done();
+ }
+
+ }, () => ({
+ userGroups: {
+ searchText: "*"
+ }
+ }));
+
+ });
+ it('getUserGroups error', (done) => {
+ const retFun = getUserGroups('MISSING_LINK', {params: {start: 0, limit: 10}});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ expect(action.type).toBe(GETGROUPS);
+ count++;
+ if (count === 2) {
+ expect(action.status).toBe(STATUS_ERROR);
+ expect(action.error).toExist();
+ done();
+ }
+
+ });
+
+ });
+ it('edit UserGroup', (done) => {
+ const retFun = editGroup({id: 1});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ expect(action.type).toBe(EDITGROUP);
+ count++;
+ if (count === 2) {
+ expect(action.group).toExist();
+ expect(action.status).toBe("success");
+ done();
+ }
+ });
+ }, {security: {user: {role: "ADMIN"}}});
+
+ it('edit UserGroup new', (done) => {
+ let template = {groupName: "hello"};
+ const retFun = editGroup(template);
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ expect(action.type).toBe(EDITGROUP);
+ count++;
+ if (count === 1) {
+ expect(action.group).toExist();
+ expect(action.group).toBe(template);
+ done();
+ }
+ });
+ });
+ it('edit UserGroup error', (done) => {
+ const retFun = editGroup({id: 99999});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ expect(action.type).toBe(EDITGROUP);
+ count++;
+ if (count === 2) {
+ expect(action.error).toExist();
+ expect(action.status).toBe("error");
+ done();
+ }
+ });
+ });
+
+ it('change usergroup metadata', () => {
+ const action = changeGroupMetadata("groupName", "New Group Name");
+ expect(action).toExist();
+ expect(action.type).toBe(EDITGROUPDATA);
+ expect(action.key).toBe("groupName");
+ expect(action.newValue).toBe("New Group Name");
+
+ });
+
+ it('update usergroup', (done) => {
+ // 1# is a workaround to skip the trailing slash of the request
+ // that can not be managed by the test-resources
+ const retFun = saveGroup({id: "1#", newUsers: [{id: 100, name: "name1"}]});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ if (action.type) {
+ expect(action.type).toBe(UPDATEGROUP);
+ }
+ count++;
+ if (count === 2) {
+ expect(action.group).toExist();
+ expect(action.status).toBe("saved");
+ }
+ if (count === 3) {
+ // the third call is for update list
+ done();
+ }
+ });
+ });
+ it('create usergroup', (done) => {
+ GeoStoreDAO.addBaseUrl = (options) => {
+ return assign(options, {baseURL: 'base/web/client/test-resources/geostore/usergroups/newGroup.txt#'});
+ };
+ const retFun = saveGroup({groupName: "TEST"});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ if (action.type) {
+ expect(action.type).toBe(UPDATEGROUP);
+ }
+ count++;
+ if (count === 2) {
+ expect(action.group).toExist();
+ expect(action.group.id).toExist();
+ expect(action.group.id).toBe(1);
+ expect(action.status).toBe("created");
+ }
+ if (count === 3) {
+ // the third call is for update list
+ done();
+ }
+ });
+ });
+ it('create usergroup with groups', (done) => {
+ GeoStoreDAO.addBaseUrl = (options) => {
+ return assign(options, {baseURL: 'base/web/client/test-resources/geostore/usergroups/newGroup.txt#'});
+ };
+ const retFun = saveGroup({groupName: "TEST", newUsers: [{id: 100, name: "name1"}]});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ if (action.type) {
+ expect(action.type).toBe(UPDATEGROUP);
+ }
+ count++;
+ if (count === 2) {
+ expect(action.group).toExist();
+ expect(action.group.id).toExist();
+ expect(action.group.id).toBe(1);
+ expect(action.status).toBe("created");
+ }
+ if (count === 3) {
+ // the third call is for update list
+ done();
+ }
+ });
+ });
+
+ it('delete Group', (done) => {
+ let confirm = deleteGroup(1);
+ expect(confirm).toExist();
+ expect(confirm.status).toBe("confirm");
+ const retFun = deleteGroup(1, "delete");
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ if (action.type) {
+ expect(action.type).toBe(DELETEGROUP);
+ }
+ count++;
+ if (count === 2) {
+ expect(action.status).toExist();
+ expect(action.status).toBe(STATUS_DELETED);
+ expect(action.id).toBe(1);
+ done();
+ }
+ if (count === 3) {
+ // the third call is for update list
+ done();
+ }
+ });
+ });
+ it('search users', (done) => {
+ const retFun = searchUsers('users.json', 0, 10, {params: {start: 0, limit: 10}}, "");
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ expect(action.type).toBe(SEARCHUSERS);
+ count++;
+ if (count === 2) {
+ expect(action.users).toExist();
+ expect(action.users[0]).toExist();
+ expect(action.users[0].groups).toExist();
+ done();
+ }
+ });
+ });
+ it('search users', (done) => {
+ const retFun = searchUsers('MISSING_LINK', {params: {start: 0, limit: 10}});
+ expect(retFun).toExist();
+ let count = 0;
+ retFun((action) => {
+ expect(action.type).toBe(SEARCHUSERS);
+ count++;
+ if (count === 2) {
+ expect(action.error).toExist();
+ done();
+ }
+ });
+ });
+});
diff --git a/web/client/actions/usergroups.js b/web/client/actions/usergroups.js
new file mode 100644
index 0000000000..3cab0db5b4
--- /dev/null
+++ b/web/client/actions/usergroups.js
@@ -0,0 +1,330 @@
+/**
+ * Copyright 2015, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const GETGROUPS = 'GROUPMANAGER_GETGROUPS';
+const EDITGROUP = 'GROUPMANAGER_EDITGROUP';
+const EDITGROUPDATA = 'GROUPMANAGER_EDITGROUP_DATA';
+const UPDATEGROUP = 'GROUPMANAGER_UPDATE_GROUP';
+const DELETEGROUP = 'GROUPMANAGER_DELETEGROUP';
+const SEARCHTEXTCHANGED = 'GROUPMANAGER_SEARCHTEXTCHANGED';
+const SEARCHUSERS = 'GROUPMANAGER_SEARCHUSERS';
+const STATUS_LOADING = "loading";
+const STATUS_SUCCESS = "success";
+const STATUS_ERROR = "error";
+// const STATUS_NEW = "new";
+const STATUS_SAVING = "saving";
+const STATUS_SAVED = "saved";
+const STATUS_CREATING = "creating";
+const STATUS_CREATED = "created";
+const STATUS_DELETED = "deleted";
+
+/*
+const USERGROUPMANAGER_UPDATE_GROUP = 'USERMANAGER_UPDATE_GROUP';
+const USERGROUPMANAGER_DELETE_GROUP = 'USERMANAGER_DELETE_GROUP';
+const USERGROUPMANAGER_SEARCH_TEXT_CHANGED = 'USERGROUPMANAGER_SEARCH_TEXT_CHANGED';
+*/
+const API = require('../api/GeoStoreDAO');
+const {get/*, assign*/} = require('lodash');
+
+function getUserGroupsLoading(text, start, limit) {
+ return {
+ type: GETGROUPS,
+ status: STATUS_LOADING,
+ searchText: text,
+ start,
+ limit
+ };
+}
+function getUserGroupSuccess(text, start, limit, groups, totalCount) {
+ return {
+ type: GETGROUPS,
+ status: STATUS_SUCCESS,
+ searchText: text,
+ start,
+ limit,
+ groups,
+ totalCount
+
+ };
+}
+function getUserGroupError(text, start, limit, error) {
+ return {
+ type: GETGROUPS,
+ status: STATUS_ERROR,
+ searchText: text,
+ start,
+ limit,
+ error
+ };
+}
+function getUserGroups(searchText, options) {
+ let params = options && options.params;
+ let start;
+ let limit;
+ if (params) {
+ start = params.start;
+ limit = params.limit;
+ }
+ return (dispatch, getState) => {
+ let text = searchText;
+ let state = getState && getState();
+ if (state) {
+ let oldText = get(state, "usergroups.searchText");
+ text = searchText || oldText || "*";
+ start = ( (start !== null && start !== undefined) ? start : (get(state, "usergroups.start") || 0));
+ limit = limit || get(state, "usergroups.limit") || 12;
+ }
+ dispatch(getUserGroupsLoading(text, start, limit));
+
+ return API.getGroups(text, {...options, params: {start, limit}}).then((response) => {
+ let groups;
+ // this because _.get returns an array with an undefined element isntead of null
+ if (!response || !response.ExtGroupList || !response.ExtGroupList.Group) {
+ groups = [];
+ } else {
+ groups = get(response, "ExtGroupList.Group");
+ }
+
+ let totalCount = get(response, "ExtGroupList.GroupCount");
+ groups = Array.isArray(groups) ? groups : [groups];
+ dispatch(getUserGroupSuccess(text, start, limit, groups, totalCount));
+ }).catch((error) => {
+ dispatch(getUserGroupError(text, start, limit, error));
+ });
+ };
+}
+function editGroupLoading(group) {
+ return {
+ type: EDITGROUP,
+ status: STATUS_LOADING,
+ group
+ };
+}
+
+function editGroupSuccess(group) {
+ return {
+ type: EDITGROUP,
+ status: STATUS_SUCCESS,
+ group
+ };
+}
+
+function editGroupError(group, error) {
+ return {
+ type: EDITGROUP,
+ status: STATUS_ERROR,
+ group,
+ error
+ };
+}
+
+function editNewGroup(group) {
+ return {
+ type: EDITGROUP,
+ group
+ };
+}
+// NOTE: not support on server side now for editing groups
+function editGroup(group, options ={params: {includeattributes: true}} ) {
+ return (dispatch) => {
+ if (group && group.id) {
+ dispatch(editGroupLoading(group));
+ return API.getGroup(group.id, options).then((groupLoaded) => {
+ // the service returns restUsers = "", skip this to avoid overriding
+ dispatch(editGroupSuccess(groupLoaded));
+ }).catch((error) => {
+ dispatch(editGroupError(group, error));
+ });
+ }
+ dispatch(editNewGroup(group));
+ };
+}
+function changeGroupMetadata(key, newValue) {
+ return {
+ type: EDITGROUPDATA,
+ key,
+ newValue
+ };
+}
+
+function savingGroup(group) {
+ return {
+ type: UPDATEGROUP,
+ status: STATUS_SAVING,
+ group
+ };
+}
+
+function savedGroup(group) {
+ return {
+ type: UPDATEGROUP,
+ status: STATUS_SAVED,
+ group: group
+ };
+}
+
+function saveError(group, error) {
+ return {
+ type: UPDATEGROUP,
+ status: STATUS_ERROR,
+ group,
+ error
+ };
+}
+
+function creatingGroup(group) {
+ return {
+ type: UPDATEGROUP,
+ status: STATUS_CREATING,
+ group
+ };
+}
+
+function groupCreated(id, group) {
+ return {
+ type: UPDATEGROUP,
+ status: STATUS_CREATED,
+ group: { ...group, id}
+ };
+}
+
+function createError(group, error) {
+ return {
+ type: UPDATEGROUP,
+ status: STATUS_ERROR,
+ group,
+ error
+ };
+}
+function saveGroup(group, options = {}) {
+ return (dispatch) => {
+ if (group && group.id) {
+ dispatch(savingGroup(group));
+ return API.updateGroupMembers(group, options).then((groupDetails) => {
+ dispatch(savedGroup(groupDetails));
+ dispatch(getUserGroups());
+ }).catch((error) => {
+ dispatch(saveError(group, error));
+ });
+ }
+ // create Group
+ dispatch(creatingGroup(group));
+ return API.createGroup(group, options).then((id) => {
+ dispatch(groupCreated(id, group));
+ dispatch(getUserGroups());
+ }).catch((error) => {
+ dispatch(createError(group, error));
+ });
+
+ };
+}
+
+function deletingGroup(id) {
+ return {
+ type: DELETEGROUP,
+ status: "deleting",
+ id
+ };
+}
+function deleteGroupSuccess(id) {
+ return {
+ type: DELETEGROUP,
+ status: STATUS_DELETED,
+ id
+ };
+}
+function deleteGroupError(id, error) {
+ return {
+ type: DELETEGROUP,
+ status: STATUS_ERROR,
+ id,
+ error
+ };
+}
+
+function closeDelete(status, id) {
+ return {
+ type: DELETEGROUP,
+ status,
+ id
+ };
+}
+function deleteGroup(id, status = "confirm") {
+ if (status === "confirm" || status === "cancelled") {
+ return closeDelete(status, id);
+ } else if ( status === "delete") {
+ return (dispatch) => {
+ dispatch(deletingGroup(id));
+ API.deleteGroup(id).then(() => {
+ dispatch(deleteGroupSuccess(id));
+ dispatch(getUserGroups());
+ }).catch((error) => {
+ dispatch(deleteGroupError(id, error));
+ });
+ };
+ }
+}
+
+function groupSearchTextChanged(text) {
+ return {
+ type: SEARCHTEXTCHANGED,
+ text
+ };
+}
+function searchUsersSuccessLoading() {
+ return {
+ type: SEARCHUSERS,
+ status: STATUS_LOADING
+ };
+}
+function searchUsersSuccess(users) {
+ return {
+ type: SEARCHUSERS,
+ status: STATUS_SUCCESS,
+ users
+ };
+}
+function searchUsersError(error) {
+ return {
+ type: SEARCHUSERS,
+ status: STATUS_ERROR,
+ error
+ };
+}
+function searchUsers(text ="*", start = 0, limit = 5, options = {}, jollyChar = "*") {
+ return (dispatch) => {
+ dispatch(searchUsersSuccessLoading(text, start, limit));
+ return API.getUsers(jollyChar + text + jollyChar, {...options, params: {start, limit}}).then((response) => {
+ let users;
+ // this because _.get returns an array with an undefined element instead of null
+ if (!response || !response.ExtUserList || !response.ExtUserList.User) {
+ users = [];
+ } else {
+ users = get(response, "ExtUserList.User");
+ }
+ users = Array.isArray(users) ? users : [users];
+ dispatch(searchUsersSuccess(users));
+ }).catch((error) => {
+ dispatch(searchUsersError(error));
+ });
+ };
+}
+
+module.exports = {
+ getUserGroups, GETGROUPS,
+ editGroup, EDITGROUP,
+ changeGroupMetadata, EDITGROUPDATA,
+ groupSearchTextChanged, SEARCHTEXTCHANGED,
+ searchUsers, SEARCHUSERS,
+ saveGroup, UPDATEGROUP,
+ deleteGroup, DELETEGROUP,
+ STATUS_SUCCESS,
+ STATUS_LOADING,
+ STATUS_ERROR,
+ STATUS_DELETED
+};
diff --git a/web/client/api/GeoStoreDAO.js b/web/client/api/GeoStoreDAO.js
index 4a724b63f4..d3149b9721 100644
--- a/web/client/api/GeoStoreDAO.js
+++ b/web/client/api/GeoStoreDAO.js
@@ -223,7 +223,70 @@ var Api = {
deleteUser: function(id, options = {}) {
let url = "users/user/" + id;
return axios.delete(url, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; });
+ },
+ getGroups: function(textSearch, options = {}) {
+ let url = "extjs/search/groups" + (textSearch ? "/" + textSearch : "");
+ return axios.get(url, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; });
+ },
+ getGroup: function(id, options = {}) {
+ let url = "usergroups/group/" + id;
+ return axios.get(url, this.addBaseUrl(parseOptions(options))).then(function(response) {
+ let groupLoaded = response.data.UserGroup;
+ let users = groupLoaded && groupLoaded.restUsers && groupLoaded.restUsers.User;
+ return {...groupLoaded, users: users && (Array.isArray(users) ? users : [users]) || []};
+ });
+ },
+ createGroup: function(group, options) {
+ let url = "usergroups/";
+ let groupId;
+ return axios.post(url, {UserGroup: {...group}}, this.addBaseUrl(parseOptions(options)))
+ .then(function(response) {
+ groupId = response.data;
+ return Api.updateGroupMembers({...group, id: groupId}, options);
+ }).then(() => groupId);
+ },
+ updateGroupMembers: function(group, options) {
+ // No GeoStore API to update group name and description. only update new users
+ if (group.newUsers) {
+ let restUsers = group.users || (group.restUsers && group.restUsers.User) || [];
+ restUsers = Array.isArray(restUsers) ? restUsers : [restUsers];
+ // old users not present in the new users list
+ let toRemove = restUsers.filter( (user) => group.newUsers.findIndex( u => u.id === user.id) < 0);
+ // new users not present in the old users list
+ let toAdd = group.newUsers.filter( (user) => restUsers.findIndex( u => u.id === user.id) < 0);
+
+ // create callbacks
+ let removeCallbacks = toRemove.map( (user) => () => this.removeUserFromGroup(user.id, group.id, options) );
+ let addCallbacks = toAdd.map( (user) => () => this.addUserToGroup(user.id, group.id), options );
+ let requests = [...(removeCallbacks.map( call => call.call(this))), ...(addCallbacks.map(call => call()))];
+ return axios.all(requests).then(() => {
+ return {
+ ...group,
+ newUsers: null,
+ restUsers: { User: group.newUsers},
+ users: group.newUsers
+ };
+ });
+ }
+ return new Promise( (resolve) => {
+ resolve({
+ ...group
+ });
+ });
+ },
+ deleteGroup: function(id, options={}) {
+ let url = "usergroups/group/" + id;
+ return axios.delete(url, this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; });
+ },
+ addUserToGroup(userId, groupId, options = {}) {
+ let url = "/usergroups/group/" + userId + "/" + groupId + "/";
+ return axios.post(url, null, this.addBaseUrl(parseOptions(options)));
+ },
+ removeUserFromGroup(userId, groupId, options = {}) {
+ let url = "/usergroups/group/" + userId + "/" + groupId + "/";
+ return axios.delete(url, this.addBaseUrl(parseOptions(options)));
}
+
};
module.exports = Api;
diff --git a/web/client/components/manager/users/GroupCard.jsx b/web/client/components/manager/users/GroupCard.jsx
new file mode 100644
index 0000000000..642d6ec54b
--- /dev/null
+++ b/web/client/components/manager/users/GroupCard.jsx
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const React = require('react');
+// const Message = require('../I18N/Message');
+const GridCard = require('../../misc/GridCard');
+const {Button, Glyphicon} = require('react-bootstrap');
+const Message = require('../../../components/I18N/Message');
+
+
+// const ConfirmModal = require('./modals/ConfirmModal');
+
+require('./style/usercard.css');
+
+const GroupCard = React.createClass({
+ propTypes: {
+ // props
+ style: React.PropTypes.object,
+ group: React.PropTypes.object,
+ innerItemStyle: React.PropTypes.object,
+ actions: React.PropTypes.array
+ },
+ getDefaultProps() {
+ return {
+ style: {
+ background: "#F7F4ED",
+ position: "relative",
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "repeat-x"
+ },
+ innerItemStyle: {"float": "left", margin: "10px"}
+ };
+ },
+ renderStatus() {
+ return (
+
+ {this.props.group.enabled ?
+
:
+
}
+
);
+ },
+ renderAvatar() {
+ return ();
+ },
+ renderDescription() {
+ return (
+
+
{this.props.group.description ? this.props.group.description : }
+
);
+ },
+ render() {
+ return (
+
+ {this.renderAvatar()}
+ {this.renderStatus()}
+ {this.renderDescription()}
+
+ );
+ }
+});
+
+module.exports = GroupCard;
diff --git a/web/client/components/manager/users/GroupDialog.jsx b/web/client/components/manager/users/GroupDialog.jsx
new file mode 100644
index 0000000000..1db428c68f
--- /dev/null
+++ b/web/client/components/manager/users/GroupDialog.jsx
@@ -0,0 +1,228 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+ /**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const React = require('react');
+const UsersTable = require('./UsersTable');
+const {Alert, Tabs, Tab, Button, Glyphicon, Input} = require('react-bootstrap');
+
+const Dialog = require('../../../components/misc/Dialog');
+const assign = require('object-assign');
+const Message = require('../../../components/I18N/Message');
+const Spinner = require('react-spinkit');
+const Select = require("react-select");
+require('./style/userdialog.css');
+ /**
+ * A Modal window to show password reset form
+ */
+const GroupDialog = React.createClass({
+ propTypes: {
+ // props
+ group: React.PropTypes.object,
+ users: React.PropTypes.array,
+ availableUsers: React.PropTypes.array,
+ searchUsers: React.PropTypes.func,
+ availableUsersLoading: React.PropTypes.bool,
+ show: React.PropTypes.bool,
+ onClose: React.PropTypes.func,
+ onChange: React.PropTypes.func,
+ onSave: React.PropTypes.func,
+ modal: React.PropTypes.bool,
+ closeGlyph: React.PropTypes.string,
+ style: React.PropTypes.object,
+ buttonSize: React.PropTypes.string,
+ inputStyle: React.PropTypes.object
+ },
+ getDefaultProps() {
+ return {
+ group: {},
+ availableUsers: [],
+ onClose: () => {},
+ onChange: () => {},
+ onSave: () => {},
+ options: {},
+ useModal: true,
+ closeGlyph: "",
+ style: {},
+ buttonSize: "large",
+ includeCloseButton: true,
+ inputStyle: {
+ height: "32px",
+ width: "260px",
+ marginTop: "3px",
+ marginBottom: "20px",
+ padding: "5px",
+ border: "1px solid #078AA3"
+ }
+ };
+ },
+ getCurrentGroupMembers() {
+ return this.props.group && (this.props.group.newUsers || this.props.group.users) || [];
+ },
+ renderGeneral() {
+ return (
+ }
+ onChange={this.handleChange}
+ value={this.props.group && this.props.group.groupName}/>
+ }
+ onChange={this.handleChange}
+ value={this.props.group && this.props.group.description || ""}/>
+
);
+ },
+
+ renderSaveButtonContent() {
+ let defaultMessage = this.props.group && this.props.group.id ? : ;
+ let messages = {
+ error: defaultMessage,
+ success: defaultMessage,
+ modified: defaultMessage,
+ save: ,
+ saving: ,
+ saved: ,
+ creating: ,
+ created:
+ };
+ let message = messages[status] || defaultMessage;
+ return [this.isSaving() ? : null, message];
+ },
+ renderButtons() {
+ return [
+ ,
+
+
+ ];
+ },
+
+ renderError() {
+ let error = this.props.group && this.props.group.status === "error";
+ if ( error ) {
+ let lastError = this.props.group && this.props.group.lastError;
+ return {lastError && lastError.statusText};
+ }
+
+ },
+ renderMembers() {
+ let members = this.getCurrentGroupMembers();
+ if (!members || members.length === 0) {
+ return (
);
+ }
+ // NOTE: faking group Id
+ return ( u1.name > u2.name)} onRemove={(user) => {
+ let id = user.id;
+ let newUsers = this.getCurrentGroupMembers().filter(u => u.id !== id);
+ this.props.onChange("newUsers", newUsers);
+ }}/>);
+ },
+ renderMembersTab() {
+ let availableUsers = this.props.availableUsers.filter((user) => this.getCurrentGroupMembers().findIndex( member => member.id === user.id) < 0).map(u => ({value: u.id, label: u.name}));
+ return (
+
+
{this.renderMembers()}
+
+
+
+
);
+ },
+ render() {
+ return ();
+ },
+ isSaving() {
+ return this.props.group && this.props.group.status === "saving";
+ },
+ isSaved() {
+ return this.props.group && (this.props.group.status === "saved" || this.props.group.status === "created");
+ },
+ isValid() {
+ let valid = true;
+ let group = this.props.group;
+ if (!group) return false;
+ valid = valid && group.groupName && group.status === "modified";
+ return valid;
+ },
+ handleChange(event) {
+ this.props.onChange(event.target.name, event.target.value);
+ }
+
+});
+
+module.exports = GroupDialog;
diff --git a/web/client/components/manager/users/GroupGrid.jsx b/web/client/components/manager/users/GroupGrid.jsx
new file mode 100644
index 0000000000..15feba7930
--- /dev/null
+++ b/web/client/components/manager/users/GroupGrid.jsx
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const React = require('react');
+const {Grid, Row, Col} = require('react-bootstrap');
+const GroupCard = require('./GroupCard');
+const Spinner = require('react-spinkit');
+const Message = require('../../I18N/Message');
+var GroupsGrid = React.createClass({
+ propTypes: {
+ loadGroups: React.PropTypes.func,
+ onEdit: React.PropTypes.func,
+ onDelete: React.PropTypes.func,
+ myUserId: React.PropTypes.number,
+ fluid: React.PropTypes.bool,
+ groups: React.PropTypes.array,
+ loading: React.PropTypes.bool,
+ bottom: React.PropTypes.node,
+ colProps: React.PropTypes.object
+ },
+ getDefaultProps() {
+ return {
+ loadGroups: () => {},
+ onEdit: () => {},
+ onDelete: () => {},
+ fluid: true,
+ colProps: {
+ xs: 12,
+ sm: 6,
+ md: 4,
+ lg: 3,
+ style: {
+ "marginBottom": "20px"
+ }
+ }
+ };
+ },
+ componentDidMount() {
+ this.props.loadGroups();
+ },
+ renderLoading() {
+ if (this.props.loading) {
+ return ();
+ }
+
+ },
+ renderGroups(groups) {
+ return groups.map((group) => {
+ let actions = [{
+ onClick: () => {this.props.onEdit(group); },
+ glyph: "wrench",
+ tooltip:
+ }, {
+ onClick: () => {this.props.onDelete(group && group.id); },
+ glyph: "remove-circle",
+ tooltip:
+ }];
+ if ( group && group.groupName === "everyone") {
+ actions = [];
+ }
+
+ return ;
+ });
+ },
+ render: function() {
+ return (
+
+ {this.renderLoading()}
+
+ {this.renderGroups(this.props.groups || [])}
+
+
+ {this.props.bottom}
+
+
+ );
+ }
+});
+
+module.exports = GroupsGrid;
diff --git a/web/client/components/manager/users/UsersTable.jsx b/web/client/components/manager/users/UsersTable.jsx
new file mode 100644
index 0000000000..110970b8a1
--- /dev/null
+++ b/web/client/components/manager/users/UsersTable.jsx
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const React = require('react');
+const {Button, Glyphicon, Table, OverlayTrigger, Tooltip} = require('react-bootstrap');
+const Message = require('../../I18N/Message');
+var UsersTable = React.createClass({
+ propTypes: {
+ users: React.PropTypes.array,
+ deleteToolTip: React.PropTypes.string,
+ onRemove: React.PropTypes.func
+ },
+ getDefaultProps() {
+ return {
+ users: [],
+ deleteToolTip: "usergroups.removeUser",
+ onRemove: () => {}
+ };
+ },
+ render: function() {
+ return ({this.props.users.map((user) => {
+ let tooltip = ;
+ return (
+ {user.name} |
+
+
+
+
+ |
+
+ );
+ })}
);
+ }
+});
+
+module.exports = UsersTable;
diff --git a/web/client/components/manager/users/__tests__/GroupCard-test.jsx b/web/client/components/manager/users/__tests__/GroupCard-test.jsx
new file mode 100644
index 0000000000..dafd8bdb97
--- /dev/null
+++ b/web/client/components/manager/users/__tests__/GroupCard-test.jsx
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require("react");
+const expect = require('expect');
+const ReactDOM = require('react-dom');
+const GroupCard = require('../GroupCard');
+var ReactTestUtils = require('react-addons-test-utils');
+const group1 = {
+ id: 1,
+ groupName: "GROUP1",
+ description: "description",
+ enabled: true,
+ users: [{
+ name: "USER1",
+ id: 100
+ }]
+};
+
+describe("Test GroupCard Component", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('Test group rendering', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ let title = ReactTestUtils.scryRenderedDOMComponentsWithClass(
+ comp,
+ "gridcard-title"
+ );
+ expect(title.length).toBe(1);
+ expect(ReactTestUtils.scryRenderedDOMComponentsWithClass(
+ comp,
+ "group-thumb-description"
+ ).length).toBe(1);
+ });
+});
diff --git a/web/client/components/manager/users/__tests__/GroupDialog-test.jsx b/web/client/components/manager/users/__tests__/GroupDialog-test.jsx
new file mode 100644
index 0000000000..035b96c8e6
--- /dev/null
+++ b/web/client/components/manager/users/__tests__/GroupDialog-test.jsx
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require("react");
+const expect = require('expect');
+const ReactDOM = require('react-dom');
+const GroupDialog = require('../GroupDialog');
+const user1 = {
+ id: 100,
+ name: "USER2",
+ role: "USER",
+ enabled: false,
+ groups: [{
+ id: 1,
+ groupName: "GROUP1"
+ }]
+};
+const user2 = {
+ id: 101,
+ name: "ADMIN",
+ role: "ADMIN",
+ enabled: true,
+ groups: [{
+ id: 1,
+ groupName: "GROUP1"
+ }]
+};
+const group1 = {
+ id: 1,
+ groupName: "GROUP1",
+ description: "description",
+ enabled: true,
+ users: [user1, user2]
+};
+const users = [ user1, user2 ];
+describe("Test UserDialog Component", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('Test group rendering', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ });
+
+ it('Test group loading', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ });
+ it('Test group error', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ });
+ it('Test group dialog with users', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ });
+ it('Test group dialog with new users', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ });
+});
diff --git a/web/client/components/manager/users/__tests__/GroupGrid-test.jsx b/web/client/components/manager/users/__tests__/GroupGrid-test.jsx
new file mode 100644
index 0000000000..b7b5a96d49
--- /dev/null
+++ b/web/client/components/manager/users/__tests__/GroupGrid-test.jsx
@@ -0,0 +1,97 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require("react");
+const expect = require('expect');
+const ReactDOM = require('react-dom');
+var ReactTestUtils = require('react-addons-test-utils');
+const GroupGrid = require('../GroupGrid');
+const group1 = {
+ id: 1,
+ groupName: "GROUP1",
+ description: "description",
+ enabled: true,
+ users: [{
+ name: "USER1",
+ id: 100
+ }]
+};
+
+describe("Test GroupGrid Component", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('Test group grid rendering', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ let domNode = ReactDOM.findDOMNode(comp);
+ expect(domNode.className).toBe("container-fluid");
+ let rows = ReactTestUtils.scryRenderedDOMComponentsWithClass(
+ comp,
+ "row"
+ );
+ expect(rows).toExist();
+ expect(rows.length).toBe(2);
+ let card = ReactTestUtils.scryRenderedDOMComponentsWithClass(comp, "gridcard");
+ expect(card).toExist();
+ expect(card.length).toBe(1);
+ let buttons = ReactTestUtils.scryRenderedDOMComponentsWithClass(
+ comp,
+ "gridcard-button"
+ );
+ ReactTestUtils.Simulate.click(buttons[0]);
+ ReactTestUtils.Simulate.click(buttons[1]);
+ expect(buttons.length).toBe(2);
+ });
+ it('Test everyone\'s group rendering in grid', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ let domNode = ReactDOM.findDOMNode(comp);
+ expect(domNode.className).toBe("container-fluid");
+ let buttons = ReactTestUtils.scryRenderedDOMComponentsWithClass(
+ comp,
+ "gridcard-button"
+ );
+ expect(buttons.length).toBe(0);
+ });
+ it('Test group grid events', () => {
+ const testHandlers = {
+ onEdit: () => {},
+ onDelete: () => {}
+ };
+ const spyEdit = expect.spyOn(testHandlers, 'onEdit');
+ const spyDelete = expect.spyOn(testHandlers, 'onDelete');
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ let domNode = ReactDOM.findDOMNode(comp);
+ expect(domNode.className).toBe("container-fluid");
+ let buttons = ReactTestUtils.scryRenderedDOMComponentsWithClass(
+ comp,
+ "gridcard-button"
+ );
+ expect(buttons.length).toBe(2);
+ ReactTestUtils.Simulate.click(buttons[0]);
+ ReactTestUtils.Simulate.click(buttons[1]);
+ expect(spyEdit.calls.length).toEqual(1);
+ expect(spyDelete.calls.length).toEqual(1);
+ });
+
+});
diff --git a/web/client/components/manager/users/__tests__/UserTable-test.jsx b/web/client/components/manager/users/__tests__/UserTable-test.jsx
new file mode 100644
index 0000000000..fb2bd09a11
--- /dev/null
+++ b/web/client/components/manager/users/__tests__/UserTable-test.jsx
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require("react");
+const expect = require('expect');
+const ReactDOM = require('react-dom');
+const UserTable = require('../UsersTable');
+var ReactTestUtils = require('react-addons-test-utils');
+
+const users = [{
+ id: 2,
+ name: "USER2",
+ role: "USER",
+ enabled: false,
+ groups: [{
+ groupName: "GROUP1"
+ }]
+ }, {
+ id: 3,
+ name: "ADMIN",
+ role: "ADMIN",
+ enabled: true
+}];
+describe("Test UsersTable Component", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('Test usergroup table', () => {
+ let comp = ReactDOM.render(
+ , document.getElementById("container"));
+ expect(comp).toExist();
+ let table = ReactTestUtils.scryRenderedDOMComponentsWithTag(comp, "table");
+ expect(table.length).toBe(1);
+ let rows = ReactTestUtils.scryRenderedDOMComponentsWithTag(comp, "tr");
+ expect(rows.length).toBe(2);
+ });
+});
diff --git a/web/client/components/manager/users/style/usercard.css b/web/client/components/manager/users/style/usercard.css
index 279d136df1..0fa06a8a7f 100644
--- a/web/client/components/manager/users/style/usercard.css
+++ b/web/client/components/manager/users/style/usercard.css
@@ -1,26 +1,27 @@
-.user-thumb {
+.user-thumb, .group-thumb {
max-width: 100%;
overflow: hidden;
height: 180px;
font-size: 12px;
cursor: auto;
}
-.user-thumb .gridcard-title {
+.user-thumb .gridcard-title, .group-thumb .gridcard-title {
min-height: 25px;
}
-.user-thumb .gridcard-tools {
+.user-thumb .gridcard-tools,.group-thumb .gridcard-tools {
right: 0
}
-.user-thumb-description {
+.group-thumb .group-thumb-description {
/* ellipsis for description */
+ margin: 10px;
display: block; /* Fallback for non-webkit */
display: -webkit-box;
padding: 5px;
magin: 0 auto;
- height: 80px; /* font-size * webkit-line-clamp + padding */
- font-size: 15px;
+ height: 75px; /* font-size * webkit-line-clamp + padding */
+ font-size: 12px;
line-height: 1;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
diff --git a/web/client/components/manager/users/style/userdialog.css b/web/client/components/manager/users/style/userdialog.css
index 00d03529f3..2cbaee93b3 100644
--- a/web/client/components/manager/users/style/userdialog.css
+++ b/web/client/components/manager/users/style/userdialog.css
@@ -1,12 +1,19 @@
-.user-edit-dialog{
+.user-edit-dialog, .group-edit-dialog{
background: white;
z-index: 1000;
}
-.user-edit-dialog {
+.user-edit-dialog,.group-edit-dialog {
max-width: 300px;
}
-.user-edit-dialog .modal-body .nav-tabs li a {
+.user-edit-dialog .modal-body .nav-tabs li a, .group-edit-dialog .modal-body .nav-tabs li a {
padding: 0;
}
+.group-edit-dialog .user-thumb {
+ height: auto;
+ margin-bottom: 10px;
+}
+.group-edit-dialog .user-thumb .user-status {
+ display: none;
+}
diff --git a/web/client/plugins/manager/UserManager.jsx b/web/client/plugins/manager/UserManager.jsx
index eacc80b4f3..88e0b78856 100644
--- a/web/client/plugins/manager/UserManager.jsx
+++ b/web/client/plugins/manager/UserManager.jsx
@@ -6,27 +6,42 @@
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
+const {connect} = require('react-redux');
const SearchBar = require('./users/SearchBar');
const UserGrid = require('./users/UserGrid');
+const GroupsGrid = require('./users/GroupGrid');
const UserDialog = require('./users/UserDialog');
+const GroupDialog = require('./users/GroupDialog');
const TopButtons = require('./users/TopButtons');
const UserDeleteConfirm = require('./users/UserDeleteConfirm');
+const GroupDeleteConfirm = require('./users/GroupDeleteConfirm');
const Message = require('../../components/I18N/Message');
const assign = require('object-assign');
const UserManager = React.createClass({
+ propTypes: {
+ selectedTool: React.PropTypes.string
+ },
+ getDefaultProps() {
+ return {
+ selectedTool: "users"
+ };
+ },
render() {
return (
-
+ {this.props.selectedTool === "users" ? : }
+
+
);
}
});
module.exports = {
- UserManagerPlugin: assign(UserManager, {
+ UserManagerPlugin: assign(
+ connect((state) => ({ selectedTool: state && state.controls && state.controls.usermanager && state.controls.usermanager.selectedTool}))(UserManager), {
hide: true,
Manager: {
id: "usermanager",
@@ -36,6 +51,8 @@ module.exports = {
glyph: "1-group-mod"
}}),
reducers: {
- users: require('../../reducers/users')
+ users: require('../../reducers/users'),
+ usergroups: require('../../reducers/usergroups'),
+ controls: require('../../reducers/controls')
}
};
diff --git a/web/client/plugins/manager/style/manager.css b/web/client/plugins/manager/style/manager.css
index 2c35fff25b..4eb489f12e 100644
--- a/web/client/plugins/manager/style/manager.css
+++ b/web/client/plugins/manager/style/manager.css
@@ -22,3 +22,6 @@
order: -1;
max-width: 20%;
}
+.Manager-Container .usermanager-title {
+ margin-top: 0;
+}
diff --git a/web/client/plugins/manager/users/GroupDeleteConfirm.jsx b/web/client/plugins/manager/users/GroupDeleteConfirm.jsx
new file mode 100644
index 0000000000..8a4e5ce43c
--- /dev/null
+++ b/web/client/plugins/manager/users/GroupDeleteConfirm.jsx
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require('react');
+const {connect} = require('react-redux');
+const {deleteGroup} = require('../../../actions/usergroups');
+const {Alert} = require('react-bootstrap');
+const Confirm = require('../../../components/misc/ConfirmDialog');
+const GroupCard = require('../../../components/manager/users/GroupCard');
+const Message = require('../../../components/I18N/Message');
+
+const GroupDeleteConfirm = React.createClass({
+ propTypes: {
+ group: React.PropTypes.object,
+ deleteGroup: React.PropTypes.func,
+ deleteId: React.PropTypes.number,
+ deleteError: React.PropTypes.object,
+ deleteStatus: React.PropTypes.string
+
+ },
+ getDefaultProps() {
+ return {
+ deleteGroup: () => {}
+ };
+ },
+ renderError() {
+ if (this.props.deleteError) {
+ return {this.props.deleteError.statusText};
+ }
+ },
+ renderConfirmButtonContent() {
+ switch (this.props.deleteStatus) {
+ case "deleting":
+ return ;
+ default:
+ return ;
+ }
+ },
+ render() {
+ if (!this.props.group) {
+ return null;
+ }
+ return ( this.props.deleteGroup(this.props.deleteId, "cancelled")}
+ onConfirm={ () => { this.props.deleteGroup(this.props.deleteId, "delete"); } }
+ confirmButtonContent={this.renderConfirmButtonContent()}
+ confirmButtonDisabled={this.props.deleteStatus === "deleting"}>
+
+
+ {this.renderError()}
+ );
+ }
+});
+module.exports = connect((state) => {
+ let groupsstate = state && state.usergroups;
+ if (!groupsstate) return {};
+ let groups = groupsstate && groupsstate.groups;
+ let deleteId = groupsstate.deletingGroup && groupsstate.deletingGroup.id;
+ if (groups && deleteId) {
+ let index = groups.findIndex((user) => user.id === deleteId);
+ let group = groups[index];
+ return {
+ group,
+ deleteId,
+ deleteError: groupsstate.deletingGroup.error,
+ deleteStatus: groupsstate.deletingGroup.status
+ };
+ }
+ return {
+ deleteId
+ };
+}, {deleteGroup} )(GroupDeleteConfirm);
diff --git a/web/client/plugins/manager/users/GroupDialog.jsx b/web/client/plugins/manager/users/GroupDialog.jsx
new file mode 100644
index 0000000000..d5f10b7692
--- /dev/null
+++ b/web/client/plugins/manager/users/GroupDialog.jsx
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const {bindActionCreators} = require('redux');
+const {connect} = require('react-redux');
+const {editGroup, changeGroupMetadata, saveGroup, searchUsers} = require('../../../actions/usergroups');
+
+
+const mapStateToProps = (state) => {
+ const usergroups = state && state.usergroups;
+ return {
+ modal: true,
+ availableUsers: usergroups && usergroups.availableUsers,
+ availableUsersLoading: usergroups && usergroups.availableUsersLoading,
+ show: usergroups && !!usergroups.currentGroup,
+ group: usergroups && usergroups.currentGroup
+ };
+};
+const mapDispatchToProps = (dispatch) => {
+ return bindActionCreators({
+ searchUsers: searchUsers.bind(null),
+ onChange: changeGroupMetadata.bind(null),
+ onClose: editGroup.bind(null, null),
+ onSave: saveGroup.bind(null)
+ }, dispatch);
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(require('../../../components/manager/users/GroupDialog'));
diff --git a/web/client/plugins/manager/users/GroupGrid.jsx b/web/client/plugins/manager/users/GroupGrid.jsx
new file mode 100644
index 0000000000..688eb8c8f8
--- /dev/null
+++ b/web/client/plugins/manager/users/GroupGrid.jsx
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const React = require('react');
+const {bindActionCreators} = require('redux');
+const {connect} = require('react-redux');
+const assign = require('object-assign');
+const {getUserGroups, editGroup, deleteGroup} = require('../../../actions/usergroups');
+const PaginationToolbar = require('./GroupsPaginationToolbar');
+
+const mapStateToProps = (state) => {
+ const usergroups = state && state.usergroups;
+ return {
+ groups: usergroups && state.usergroups.groups,
+ loading: usergroups && (usergroups.status === "loading"),
+ stateProps: usergroups && usergroups.stateProps,
+ start: usergroups && usergroups.start,
+ limit: usergroups && usergroups.limit,
+ myUserId: state && state.security && state.security.user && state.security.user.id
+ };
+};
+const mapDispatchToProps = (dispatch) => {
+ return bindActionCreators({
+ loadGroups: getUserGroups,
+ onEdit: editGroup,
+ onDelete: deleteGroup
+ }, dispatch);
+};
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ return assign({}, stateProps, dispatchProps, ownProps, {
+ bottom: ,
+ loadGroups: () => {
+ dispatchProps.loadGroups(stateProps && stateProps.searchText, {
+ params: {
+ start: stateProps && stateProps.start || 0,
+ limit: stateProps && stateProps.limit || 12
+ }
+ });
+ }
+ });
+};
+module.exports = connect(mapStateToProps, mapDispatchToProps, mergeProps)(require('../../../components/manager/users/GroupGrid'));
diff --git a/web/client/plugins/manager/users/GroupsPaginationToolbar.jsx b/web/client/plugins/manager/users/GroupsPaginationToolbar.jsx
new file mode 100644
index 0000000000..83e53c60a6
--- /dev/null
+++ b/web/client/plugins/manager/users/GroupsPaginationToolbar.jsx
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const {connect} = require('react-redux');
+
+const {getUserGroups} = require('../../../actions/usergroups');
+const PaginationToolbar = connect((state) => {
+ if (!state.usergroups ) {
+ return {};
+ }
+ let {start, limit, groups, status, totalCount, searchText} = state.usergroups;
+ let page = 0;
+ if (groups && totalCount) { // must be !==0 and exist to do the division
+ page = Math.ceil(start / limit);
+ }
+
+ return {
+ page: page,
+ pageSize: limit,
+ items: groups,
+ total: totalCount,
+ searchText,
+ loading: status === "loading"
+ };
+}, {onSelect: getUserGroups}, (stateProps, dispatchProps) => {
+
+ return {
+ ...stateProps,
+ onSelect: (pageNumber) => {
+ let start = stateProps.pageSize * pageNumber;
+ let limit = stateProps.pageSize;
+ dispatchProps.onSelect(stateProps.searchText, {params: { start, limit}});
+ }
+ };
+})(require('../../../components/misc/PaginationToolbar'));
+module.exports = PaginationToolbar;
diff --git a/web/client/plugins/manager/users/SearchBar.jsx b/web/client/plugins/manager/users/SearchBar.jsx
index c31cf5fd2e..2ce39026e9 100644
--- a/web/client/plugins/manager/users/SearchBar.jsx
+++ b/web/client/plugins/manager/users/SearchBar.jsx
@@ -1,3 +1,4 @@
+
/**
* Copyright 2016, GeoSolutions Sas.
* All rights reserved.
@@ -9,35 +10,60 @@ const {connect} = require('react-redux');
const {getUsers, usersSearchTextChanged} = require('../../../actions/users');
+const {getUserGroups, groupSearchTextChanged} = require('../../../actions/usergroups');
const {trim} = require('lodash');
-
-const SearchBar = connect((state) => ({
- className: "user-search",
- hideOnBlur: false,
- placeholderMsgId: "users.searchUsers",
- typeAhead: false,
- start: state && state.users && state.users.start,
- limit: state && state.users && state.users.limit,
- searchText: (state.users && state.users.searchText && trim(state.users.searchText, '*')) || ""
-}), {
- onSearchTextChange: usersSearchTextChanged,
- onSearch: (text, options) => {
+const USERS = "users";
+// const GROUPS = "groups";
+const SearchBar = connect((state) => {
+ let tool = state && state.controls && state.controls.usermanager && state.controls.usermanager && state.controls.usermanager.selectedTool;
+ let searchState = tool === USERS ? (state && state.users) : (state && state.usergroups);
+ return {
+ tool,
+ className: "user-search",
+ hideOnBlur: false,
+ placeholderMsgId: tool === USERS ? "users.searchUsers" : "usergroups.searchGroups",
+ typeAhead: false,
+ start: searchState && searchState.start,
+ limit: searchState && searchState.limit,
+ searchText: (searchState && searchState.searchText && trim(searchState.searchText, '*')) || ""
+ };
+}, {
+ usersSearchTextChanged, groupSearchTextChanged,
+ onSearchUser: (text, options) => {
let searchText = (text && text !== "") ? ("*" + text + "*") : "*";
return getUsers(searchText, options);
},
- onSearchReset: getUsers.bind(null, "*")
+ onSearchGroup: (text, options) => {
+ let searchText = (text && text !== "") ? ("*" + text + "*") : "*";
+ return getUserGroups(searchText, options);
+ }
}, (stateProps, dispatchProps) => {
return {
...stateProps,
onSearch: (text) => {
let limit = stateProps.limit;
- dispatchProps.onSearch(text, {params: {start: 0, limit}});
+ if (stateProps.tool === "USER") {
+ dispatchProps.onSearchUser(text, {params: {start: 0, limit}});
+ } else {
+ dispatchProps.onSearchGroup(text, {params: {start: 0, limit}});
+ }
},
onSearchReset: () => {
+ if (stateProps.tool === "USER") {
+ dispatchProps.onSearchUser();
+ } else {
+ dispatchProps.onSearchGroup();
+ }
dispatchProps.onSearchReset({params: {start: 0, limit: stateProps.limit}});
},
- onSearchTextChange: dispatchProps.onSearchTextChange
+ onSearchTextChange: (text) => {
+ if (stateProps.tool === "USER") {
+ dispatchProps.usersSearchTextChanged(text);
+ } else {
+ dispatchProps.groupSearchTextChanged(text);
+ }
+ }
};
})(require("../../../components/mapcontrols/search/SearchBar"));
diff --git a/web/client/plugins/manager/users/TopButtons.jsx b/web/client/plugins/manager/users/TopButtons.jsx
index 0ed60c712d..205ec1bf3f 100644
--- a/web/client/plugins/manager/users/TopButtons.jsx
+++ b/web/client/plugins/manager/users/TopButtons.jsx
@@ -7,23 +7,69 @@
*/
const React = require('react');
const {connect} = require('react-redux');
-const {Button, Grid} = require('react-bootstrap');
+const {Button, Grid, Glyphicon} = require('react-bootstrap');
const {editUser} = require('../../../actions/users');
+const {editGroup} = require('../../../actions/usergroups');
+const {setControlProperty} = require('../../../actions/controls');
const Message = require('../../../components/I18N/Message');
-
+const USERS = "users";
+const GROUPS = "groups";
const Bar = React.createClass({
propTypes: {
- onNewUser: React.PropTypes.func
+ selectedTool: React.PropTypes.string,
+ onNewUser: React.PropTypes.func,
+ onNewGroup: React.PropTypes.func,
+ onToggleUsersGroups: React.PropTypes.func
},
getDefaultProps() {
return {
- onNewUser: () => {}
+ selectedTool: "users",
+ onNewUser: () => {},
+ onNewGroup: () => {},
+ onToggleUsersGroups: () => {}
};
},
+ onNew() {
+ if (this.props.selectedTool === "users") {
+ this.props.onNewUser();
+ } else if (this.props.selectedTool === "groups") {
+ this.props.onNewGroup();
+ }
+ },
+ renderNewButton() {
+ if (this.props.selectedTool === USERS) {
+ return ;
+ } else if (this.props.selectedTool === GROUPS) {
+ return ;
+ }
+ },
+ renderToggle() {
+ if (this.props.selectedTool === (USERS)) {
+ return ;
+ } else if (this.props.selectedTool === GROUPS) {
+ return ;
+ }
+ },
+ renderTitle() {
+ return
;
+ },
render() {
- return ();
+ return (
+ {this.renderTitle()}
+
+
+ );
+ },
+ toogleTools() {
+ this.props.onToggleUsersGroups(this.props.selectedTool === USERS ? GROUPS : USERS );
}
});
-const TopButtons = connect(() => ({}), {onNewUser: editUser.bind(null, {role: "USER", "enabled": true})} )(Bar);
+const TopButtons = connect((state) => ({
+ selectedTool: state && state.controls && state.controls.usermanager && state.controls.usermanager && state.controls.usermanager.selectedTool
+}), {
+ onNewUser: editUser.bind(null, {role: "USER", "enabled": true}),
+ onNewGroup: editGroup.bind(null, {}),
+ onToggleUsersGroups: setControlProperty.bind(null, "usermanager", "selectedTool")
+})(Bar);
module.exports = TopButtons;
diff --git a/web/client/plugins/manager/users/UserGrid.jsx b/web/client/plugins/manager/users/UserGrid.jsx
index f1b12e9704..a8b2bfebfb 100644
--- a/web/client/plugins/manager/users/UserGrid.jsx
+++ b/web/client/plugins/manager/users/UserGrid.jsx
@@ -10,7 +10,7 @@ const {bindActionCreators} = require('redux');
const {connect} = require('react-redux');
const assign = require('object-assign');
const {getUsers, editUser, deleteUser} = require('../../../actions/users');
-const PaginationToolbar = require('./PaginationToolbar');
+const PaginationToolbar = require('./UsersPaginationToolbar');
const mapStateToProps = (state) => {
const users = state && state.users;
diff --git a/web/client/plugins/manager/users/PaginationToolbar.jsx b/web/client/plugins/manager/users/UsersPaginationToolbar.jsx
similarity index 100%
rename from web/client/plugins/manager/users/PaginationToolbar.jsx
rename to web/client/plugins/manager/users/UsersPaginationToolbar.jsx
diff --git a/web/client/reducers/__tests__/usergroups-test.js b/web/client/reducers/__tests__/usergroups-test.js
new file mode 100644
index 0000000000..a42ff85791
--- /dev/null
+++ b/web/client/reducers/__tests__/usergroups-test.js
@@ -0,0 +1,189 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+const expect = require('expect');
+
+const usergroups = require('../usergroups');
+const {
+ GETGROUPS, EDITGROUP, EDITGROUPDATA,
+ SEARCHTEXTCHANGED, SEARCHUSERS, UPDATEGROUP, DELETEGROUP, STATUS_SUCCESS, STATUS_LOADING, STATUS_SAVED, STATUS_ERROR
+} = require('../../actions/usergroups');
+
+describe('Test the usergroups reducer', () => {
+ it('default loading', () => {
+ let oldState = {test: "test"};
+ const state = usergroups(oldState, {
+ type: "TEST_UNKNOWN_ACTION",
+ status: 'loading'
+ });
+ expect(state).toBe(oldState);
+ });
+ it('search text change', () => {
+ const state = usergroups(undefined, {
+ type: SEARCHTEXTCHANGED,
+ text: "TEXT"
+ });
+ expect(state.searchText).toBe("TEXT");
+ });
+ it('set loading', () => {
+ const state = usergroups(undefined, {
+ type: GETGROUPS,
+ status: STATUS_LOADING
+ });
+ expect(state.status).toBe('loading');
+ });
+ it('get groups', () => {
+ const state = usergroups(undefined, {
+ type: GETGROUPS,
+ status: STATUS_SUCCESS,
+ groups: [],
+ totalCount: 0
+ });
+ expect(state.groups).toExist();
+ expect(state.groups.length).toBe(0);
+ });
+ it('edit group', () => {
+ const state = usergroups(undefined, {
+ type: EDITGROUP,
+ group: {
+ groupName: "group",
+ description: "description"
+ },
+ totalCount: 0
+ });
+ expect(state.currentGroup).toExist();
+ expect(state.currentGroup.groupName).toBe("group");
+ const stateMerge = usergroups({currentGroup: {
+ id: 1
+ }}, {
+ type: EDITGROUP,
+ status: "success",
+ group: {
+ id: 1,
+ groupName: "group",
+ description: "description"
+ },
+ totalCount: 0
+ });
+ expect(stateMerge.currentGroup).toExist();
+ expect(stateMerge.currentGroup.id).toBe(1);
+ expect(stateMerge.currentGroup.groupName).toBe("group");
+
+ // action for a user not related with current.
+ let newState = usergroups(stateMerge, {
+ type: EDITGROUP,
+ status: STATUS_SUCCESS,
+ group: {
+ id: 2,
+ groupName: "group",
+ description: "description"
+ },
+ totalCount: 0
+ });
+ expect(newState).toBe(stateMerge);
+
+ });
+
+ it('edit group data', () => {
+ const state = usergroups({currentGroup: {
+ id: 1,
+ groupName: "groupName"
+ }}, {
+ type: EDITGROUPDATA,
+ key: "groupName",
+ newValue: "newGroupName"
+ });
+ expect(state.currentGroup).toExist();
+ expect(state.currentGroup.id).toBe(1);
+ expect(state.currentGroup.groupName).toBe("newGroupName");
+ const stateMerge = usergroups({currentGroup: {
+ id: 1,
+ groupName: "userName"
+ }}, {
+ type: EDITGROUPDATA,
+ key: "description",
+ newValue: "value2"
+ });
+ expect(stateMerge.currentGroup).toExist();
+ expect(stateMerge.currentGroup.id).toBe(1);
+ expect(stateMerge.currentGroup.description).toBe("value2");
+
+ // edit existing data
+ let stateMerge2 = usergroups(stateMerge, {
+ type: EDITGROUPDATA,
+ key: "description",
+ newValue: "new description"
+ });
+ expect(stateMerge2.currentGroup).toExist();
+ expect(stateMerge2.currentGroup.id).toBe(1);
+ expect(stateMerge2.currentGroup.description).toBe("new description");
+ });
+ it('update group data', () => {
+ const state = usergroups({currentGroup: {
+ id: 1,
+ groupName: "GroupName",
+ users: [{id: 10, name: "user"}]
+ }}, {
+ id: 1,
+ name: "GroupName",
+ users: [{id: 10, name: "user"}],
+ type: UPDATEGROUP,
+ status: STATUS_SAVED
+ });
+ expect(state.currentGroup).toExist();
+ expect(state.currentGroup.id).toBe(1);
+ expect(state.currentGroup.users.length).toBe(1);
+ expect(state.currentGroup.users[0].name).toBe("user");
+ });
+
+ it('delete usergroup', () => {
+ const state = usergroups({groups: [{
+ id: 1,
+ groupName: "Group",
+ users: []
+ }]}, {
+ type: DELETEGROUP,
+ id: 1,
+ status: "delete"
+ });
+ expect(state.deletingGroup).toExist();
+ const cancelledState = usergroups(state, {
+ type: DELETEGROUP,
+ id: 1,
+ status: "cancelled"
+ });
+ expect(cancelledState.deletingGroup).toBe(null);
+ });
+
+ it('get Available Users', () => {
+ const state0 = usergroups({}, {
+ type: SEARCHUSERS,
+ status: STATUS_LOADING
+ });
+ expect(state0.availableUsersLoading).toBe(true);
+ const state = usergroups({}, {
+ type: SEARCHUSERS,
+ users: [{name: "user1", id: 100}],
+ status: STATUS_SUCCESS
+ });
+ expect(state.availableUsers).toExist();
+ expect(state.availableUsers.length).toBe(1);
+ expect(state.availableUsersLoading).toBe(false);
+ const stateError = usergroups({}, {
+ type: SEARCHUSERS,
+ status: STATUS_ERROR,
+ error: "ERROR"
+ });
+ expect(stateError.availableUsersError).toBe("ERROR");
+ const stateUnchanged = usergroups(state0, {
+ type: SEARCHUSERS,
+ users: [{name: "user1", id: 100}],
+ status: "STATUS_NOT_MANAGED"
+ });
+ expect(stateUnchanged).toBe(state0);
+ });
+});
diff --git a/web/client/reducers/usergroups.js b/web/client/reducers/usergroups.js
new file mode 100644
index 0000000000..20b016dfe8
--- /dev/null
+++ b/web/client/reducers/usergroups.js
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2016, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const {
+ GETGROUPS,
+ SEARCHUSERS,
+ EDITGROUP,
+ EDITGROUPDATA,
+ DELETEGROUP,
+ UPDATEGROUP,
+ SEARCHTEXTCHANGED
+} = require('../actions/usergroups');
+const assign = require('object-assign');
+function usergroups(state = {
+ start: 0,
+ limit: 12
+}, action) {
+ switch (action.type) {
+ case GETGROUPS:
+ return assign({}, state, {
+ searchText: action.searchText,
+ status: action.status,
+ groups: action.status === "loading" ? state.groups : action.groups,
+ start: action.start,
+ limit: action.limit,
+ totalCount: action.status === "loading" ? state.totalCount : action.totalCount
+ });
+
+ case SEARCHTEXTCHANGED: {
+ return assign({}, state, {
+ searchText: action.text
+ });
+ }
+ case EDITGROUP: {
+ let newGroup = action.status ? {
+ status: action.status,
+ ...action.group
+ } : action.group;
+ if (state.currentGroup && action.group && (state.currentGroup.id === action.group.id) ) {
+ return assign({}, state, {
+ currentGroup: assign({}, state.currentGroup, {
+ status: action.status,
+ ...action.group
+ })}
+ );
+ // this to catch user loaded but window already closed
+ } else if (action.status === "loading" || action.status === "new" || !action.status) {
+ return assign({}, state, {
+ currentGroup: newGroup
+ });
+ }
+ return state;
+
+ }
+ case EDITGROUPDATA: {
+ let k = action.key;
+ let currentGroup = state.currentGroup;
+ currentGroup = assign({}, currentGroup, {[k]: action.newValue} );
+ return assign({}, state, {
+ currentGroup: assign({}, {...currentGroup, status: "modified"})
+ });
+ }
+ case UPDATEGROUP: {
+ let currentGroup = state.currentGroup;
+
+ return assign({}, state, {
+ currentGroup: assign({}, {
+ ...currentGroup,
+ ...action.group,
+ status: action.status,
+ lastError: action.error
+ })
+ });
+ }
+
+ case DELETEGROUP: {
+ if (action.status === "deleted" || action.status === "cancelled") {
+ return assign({}, state, {
+ deletingGroup: null
+ });
+ }
+ return assign({}, state, {
+ deletingGroup: {
+ id: action.id,
+ status: action.status,
+ error: action.error
+ }
+ });
+ }
+ case SEARCHUSERS: {
+ switch (action.status) {
+ case "loading": {
+ return assign({}, state, {
+ availableUsersError: null,
+ availableUsersLoading: true
+ });
+ }
+ case "success": {
+ return assign({}, state, {
+ availableUsersError: null,
+ availableUsersLoading: false,
+ availableUsers: action.users
+ });
+ }
+ case "error": {
+ return assign({}, state, {
+ availableUsersError: action.error,
+ availableUsersLoading: false
+ });
+ }
+ default:
+ return state;
+ }
+ }
+ default:
+ return state;
+ }
+}
+module.exports = usergroups;
diff --git a/web/client/test-resources/geostore/extjs/search/groups/usergroups.json b/web/client/test-resources/geostore/extjs/search/groups/usergroups.json
new file mode 100644
index 0000000000..b01de39ee1
--- /dev/null
+++ b/web/client/test-resources/geostore/extjs/search/groups/usergroups.json
@@ -0,0 +1,18 @@
+{
+ "ExtGroupList": {
+ "GroupCount": 2,
+ "Group": [
+ {
+ "description": "Group for Test",
+ "enabled": true,
+ "groupName": "TESTGROUP_1",
+ "id": 524
+ },
+ {
+ "enabled": true,
+ "groupName": "TESTGROUP_2",
+ "id": 933
+ }
+ ]
+ }
+}
diff --git a/web/client/test-resources/geostore/usergroups/group/1 b/web/client/test-resources/geostore/usergroups/group/1
new file mode 100644
index 0000000000..c794e69873
--- /dev/null
+++ b/web/client/test-resources/geostore/usergroups/group/1
@@ -0,0 +1,19 @@
+{
+ "UserGroup":{
+ "groupName":"test",
+ "id":1,
+ "restUsers":{
+ "User":{
+ "groupsNames":[
+ "test",
+ "everyone",
+ "testers",
+ "testgroup"
+ ],
+ "id":1,
+ "name":"test",
+ "role":"USER"
+ }
+ }
+ }
+}
diff --git a/web/client/test-resources/geostore/usergroups/group/100/1 b/web/client/test-resources/geostore/usergroups/group/100/1
new file mode 100644
index 0000000000..c794e69873
--- /dev/null
+++ b/web/client/test-resources/geostore/usergroups/group/100/1
@@ -0,0 +1,19 @@
+{
+ "UserGroup":{
+ "groupName":"test",
+ "id":1,
+ "restUsers":{
+ "User":{
+ "groupsNames":[
+ "test",
+ "everyone",
+ "testers",
+ "testgroup"
+ ],
+ "id":1,
+ "name":"test",
+ "role":"USER"
+ }
+ }
+ }
+}
diff --git a/web/client/test-resources/geostore/usergroups/newGroup.txt b/web/client/test-resources/geostore/usergroups/newGroup.txt
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/web/client/test-resources/geostore/usergroups/newGroup.txt
@@ -0,0 +1 @@
+1
diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE
index 20132c248f..04f57878eb 100644
--- a/web/client/translations/data.de-DE
+++ b/web/client/translations/data.de-DE
@@ -451,6 +451,7 @@
},
"users": {
"title": "Manage Accounts",
+ "users": "Users",
"searchUsers": "search for users...",
"newUser": "New User",
"editUser": "Edit user",
@@ -472,6 +473,25 @@
"errorSaving": "There was an error saving the user:",
"selectedGroups": "SELECTED GROUPS"
},
+ "usergroups": {
+ "searchGroups": "Search Groups...",
+ "groups": "Groups",
+ "removeUser": "Remove User",
+ "newGroup": "New Group",
+ "manageGroups": "Manage Groups",
+ "description": "Description:",
+ "groupName": "Group Name",
+ "groupDescription": "Description",
+ "saveGroup": "Save",
+ "createGroup": "Create",
+ "creatingGroup": "Creating...",
+ "groupMembers": "Members:",
+ "addMember": "Add Member:",
+ "noUsers": "No users for this group",
+ "errorSaving": "There was an error saving this group",
+ "errorDelete": "There was an error deleting this group",
+ "confirmDeleteGroup": "Are you sure you want to delete this group?"
+ },
"share":{
"title": "Share",
"titlePanel": "Share the map",
diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US
index b8b47a418d..ce6a0c48c4 100644
--- a/web/client/translations/data.en-US
+++ b/web/client/translations/data.en-US
@@ -451,6 +451,7 @@
},
"users": {
"title": "Manage Accounts",
+ "users": "Users",
"searchUsers": "search for users...",
"newUser": "New User",
"editUser": "Edit user",
@@ -472,6 +473,25 @@
"errorSaving": "There was an error saving the user:",
"selectedGroups": "SELECTED GROUPS"
},
+ "usergroups": {
+ "searchGroups": "Search Groups...",
+ "groups": "Groups",
+ "removeUser": "Remove User",
+ "newGroup": "New Group",
+ "manageGroups": "Manage Groups",
+ "description": "Description:",
+ "groupName": "Group Name",
+ "groupDescription": "Description",
+ "saveGroup": "Save",
+ "createGroup": "Create",
+ "creatingGroup": "Creating...",
+ "groupMembers": "Members:",
+ "addMember": "Add Member:",
+ "noUsers": "No users for this group",
+ "errorSaving": "There was an error saving this group",
+ "errorDelete": "There was an error deleting this group",
+ "confirmDeleteGroup": "Are you sure you want to delete this group?"
+ },
"share":{
"title": "Share",
"titlePanel": "Share the map",
diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR
index 741bfc0703..8126d4307a 100644
--- a/web/client/translations/data.fr-FR
+++ b/web/client/translations/data.fr-FR
@@ -453,6 +453,7 @@
},
"users": {
"title": "Gérer les comptes",
+ "users": "Utilisateurs",
"searchUsers": "rechercher des utilisateurs ...",
"newUser": "Nouvel",
"editUser": "Modifier",
@@ -474,6 +475,25 @@
"errorSaving": "Il y avait une sauver l'erreur utilisateur:",
"selectedGroups": "CERTAINS GROUPES"
},
+ "usergroups": {
+ "searchGroups": "rechercher des groupes...",
+ "groups": "Groupes",
+ "removeUser": "supprimer l'utilisateur",
+ "newGroup": "Nouveau groupe",
+ "manageGroups": "Gérer les groupes",
+ "description": "Description:",
+ "groupName": "Nom de groupe",
+ "groupDescription": "Description",
+ "saveGroup": "Sauver",
+ "createGroup": "Créer",
+ "creatingGroup": "Création...",
+ "groupMembers": "Membres:",
+ "addMember": "Ajouter un membre:",
+ "noUsers": "Aucun utilisateur pour ce groupe",
+ "errorSaving": "Une erreur s'est produite lors de l'enregistrement de ce groupe",
+ "errorDelete": "Une erreur s'est produite lors de la suppression de ce groupe",
+ "confirmDeleteGroup": "Voulez-vous vraiment supprimer ce groupe?"
+ },
"share":{
"title": "Partager",
"titlePanel": "Partager la carte",
diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT
index c510b0ccce..028cf52773 100644
--- a/web/client/translations/data.it-IT
+++ b/web/client/translations/data.it-IT
@@ -452,6 +452,7 @@
"users": {
"title": "Gestione utenti",
+ "users": "Utenti",
"searchUsers": "Cerca utenti...",
"newUser": "Nuovo utente",
"editUser": "Modifica utente",
@@ -473,6 +474,25 @@
"errorSaving": "Si è verificato un errore salvando questo utente:",
"selectedGroups": "GRUPPI SELEZIONATI"
},
+ "usergroups": {
+ "searchGroups": "Cerca Gruppi...",
+ "groups": "Gruppi",
+ "removeUser": "Rimuovi Utente",
+ "newGroup": "Nuovo Gruppo",
+ "manageGroups": "Gestisci Gruppi",
+ "description": "Descrizione:",
+ "groupName": "Nome del Gruppo",
+ "groupDescription": "Descrizione",
+ "saveGroup": "Salva",
+ "createGroup": "Crea",
+ "creatingGroup": "Creazione...",
+ "groupMembers": "Membri:",
+ "addMember": "Aggiungi Membro:",
+ "noUsers": "Nessun utente membro di questo gruppo",
+ "errorSaving": "Si è verificato un errore salvando questo gruppo:",
+ "errorDelete": "Si è verificato un errore rimuovendo questo utente:",
+ "confirmDeleteGroup": "Sei sicuro di voler cancellare questo gruppo?"
+ },
"share":{
"title": "Condividi",
"titlePanel": "Condividi la mappa",