• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 14247298254

03 Apr 2025 03:49PM UTC coverage: 76.92% (-0.05%) from 76.971%
14247298254

Pull #10925

github

web-flow
Merge ae943edcd into 646bb6e37
Pull Request #10925: #10923, #10924: Disabling GeoFence Rules and Add filter by IP for GeoFence rules

31066 of 48387 branches covered (64.2%)

61 of 109 new or added lines in 10 files covered. (55.96%)

149 existing lines in 36 files now uncovered.

38587 of 50165 relevant lines covered (76.92%)

35.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.91
/web/client/components/manager/users/GroupDialog.jsx
1
/**
2
 * Copyright 2016, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
 */
8

9
// replacement of MapStore2/web/client/components/manager/users/GroupDialog.jsx
10

11
import React from 'react';
12
import { Alert, Tabs, Tab, Glyphicon, FormControl, FormGroup, ControlLabel } from 'react-bootstrap';
13
import PropTypes from 'prop-types';
14
import assign from 'object-assign';
15
import Spinner from 'react-spinkit';
16
import { findIndex, castArray } from 'lodash';
17

18
import Button from '../../../components/misc/Button';
19
import UsersTable from './UsersTable';
20
import Dialog from '../../../components/misc/Dialog';
21
import Message from '../../../components/I18N/Message';
22
import PagedCombobox from '../../misc/combobox/PagedCombobox';
23
import CloseConfirmButton from './CloseConfirmButton';
24
import './style/userdialog.css';
25

26
import controls from './AttributeControls';
27

28

29
const PAGINATION_LIMIT = 5;
1✔
30

31
/**
32
 * A Modal window to show password reset form
33
 */
34
class GroupDialog extends React.Component {
35
    static propTypes = {
1✔
36
        // props
37
        group: PropTypes.object,
38
        users: PropTypes.array,
39
        availableUsers: PropTypes.array,
40
        availableUsersCount: PropTypes.number,
41
        searchUsers: PropTypes.func,
42
        availableUsersLoading: PropTypes.bool,
43
        showMembersTab: PropTypes.bool,
44
        showAttributesTab: PropTypes.bool,
45
        attributeFields: PropTypes.array,
46
        show: PropTypes.bool,
47
        onClose: PropTypes.func,
48
        onChange: PropTypes.func,
49
        onSave: PropTypes.func,
50
        modal: PropTypes.bool,
51
        closeGlyph: PropTypes.string,
52
        style: PropTypes.object,
53
        buttonSize: PropTypes.string,
54
        descLimit: PropTypes.number,
55
        nameLimit: PropTypes.number,
56
        inputStyle: PropTypes.object
57
    };
58

59
    static contextTypes = {
1✔
60
        intl: PropTypes.object
61
    };
62

63
    static defaultProps = {
1✔
64
        group: {},
65
        availableUsers: [],
66
        attributeFields: [{
67
            name: 'notes',
68
            controlType: "text"
69
        }],
70
        searchUsers: () => {},
71
        showMembersTab: true,
72
        showAttributesTab: true,
73
        onClose: () => {},
74
        onChange: () => {},
75
        onSave: () => {},
76
        options: {},
77
        useModal: true,
78
        closeGlyph: "",
79
        descLimit: 255,
80
        nameLimit: 255,
81
        style: {},
82
        buttonSize: "small",
83
        includeCloseButton: true,
84
        inputStyle: {
85
            height: "32px",
86
            width: "260px",
87
            marginTop: "3px",
88
            marginBottom: "20px",
89
            padding: "5px",
90
            border: "1px solid #078AA3"
91
        }
92
    };
93

94
    state = {
16✔
95
        key: 1,
96
        openSelectMember: false,
97
        selectedMember: ''
98
    };
99

100
    componentDidMount() {
101
        this.selectMemberPage = 0;
16✔
102
        this.searchUsers();
16✔
103
    }
104
    // Only to keep the selected button, not for the modal window
105

106
    getCurrentGroupMembers = () => {
16✔
107
        return this.props.group && (this.props.group.newUsers || this.props.group.users) || [];
30!
108
    };
109

110
    renderGeneral = () => {
16✔
111
        return (<div style={{clear: "both", marginTop: "10px"}}>
22✔
112
            <FormGroup>
113
                <ControlLabel><Message msgId="usergroups.groupName"/>{' '}<span style={{ fontWeight: 'bold' }}>*</span></ControlLabel>
114
                <FormControl ref="groupName"
115
                    key="groupName"
116
                    type="text"
117
                    name="groupName"
118
                    readOnly={this.props.group && this.props.group.id}
44✔
119
                    style={this.props.inputStyle}
120
                    onChange={this.handleChange}
121
                    maxLength={this.props.nameLimit}
122
                    value={this.props.group && this.props.group.groupName || ""}/>
44!
123
            </FormGroup>
124
            <FormGroup>
125
                <ControlLabel><Message msgId="usergroups.groupDescription"/></ControlLabel>
126
                <FormControl componentClass="textarea"
127
                    ref="description"
128
                    key="description"
129
                    name="description"
130
                    maxLength={this.props.descLimit}
131
                    style={this.props.inputStyle}
132
                    onChange={this.handleChange}
133
                    value={this.props.group && this.props.group.description || ""}/>
44!
134
            </FormGroup>
135
            <div style={{ fontStyle: 'italic' }}><Message msgId="users.requiredFiedsMessage"/></div>
136
        </div>);
137
    };
138

139
    renderSaveButtonContent = () => {
16✔
140
        let defaultMessage = this.props.group && this.props.group.id ? <Message key="text" msgId="usergroups.saveGroup"/> : <Message key="text" msgId="usergroups.createGroup" />;
22!
141
        let messages = {
22✔
142
            error: defaultMessage,
143
            success: defaultMessage,
144
            modified: defaultMessage,
145
            save: <Message key="text" msgId="usergroups.saveGroup"/>,
146
            saving: <Message key="text" msgId="usergroups.savingGroup" />,
147
            saved: <Message key="text" msgId="usergroups.groupSaved" />,
148
            creating: <Message key="text" msgId="usergroups.creatingGroup" />,
149
            created: <Message key="text" msgId="usergroups.groupCreated" />
150
        };
151
        let message = messages[status] || defaultMessage;
22✔
152
        return [this.isSaving() ? <Spinner key="saving-spinner" spinnerName="circle" noFadeIn overrideSpinnerClassName="spinner"/> : null, message];
22!
153
    };
154

155
    renderButtons = () => {
16✔
156
        let CloseBtn = <CloseConfirmButton status={this.props.group && this.props.group.status} onClick={this.props.onClose}/>;
22✔
157
        return [
22✔
158
            CloseBtn,
159
            <Button key="save" bsSize={this.props.buttonSize}
160
                bsStyle={this.isSaved() ? "success" : "primary" }
22!
UNCOV
161
                onClick={() => this.props.onSave(this.props.group)}
×
162
                disabled={!this.isValid() || this.isSaving()}>
23✔
163
                {this.renderSaveButtonContent()}</Button>
164
        ];
165
    };
166

167
    renderError = () => {
16✔
168
        let error = this.props.group && this.props.group.status === "error";
22✔
169
        if ( error ) {
22!
170
            let lastError = this.props.group && this.props.group.lastError;
×
171
            return <Alert key="error" bsStyle="warning"><Message msgId="usergroups.errorSaving" />{lastError && lastError.statusText}</Alert>;
×
172
        }
173
        return null;
22✔
174
    };
175

176
    renderMembers = () => {
16✔
177
        let members = this.getCurrentGroupMembers();
22✔
178
        if (!members || members.length === 0) {
22!
179
            return (<div style={{
×
180
                width: "100%",
181
                textAlign: "center"
182
            }}><Message msgId="usergroups.noUsers"/></div>);
183
        }
184
        // NOTE: faking group Id
185
        return (<UsersTable users={[...members].sort((u1, u2) => u1.name > u2.name)} onRemove={(user) => {
22✔
186
            let id = user.id;
1✔
187
            let newUsers = this.getCurrentGroupMembers().filter(u => u.id !== id);
2✔
188
            this.props.onChange("newUsers", newUsers);
1✔
189
        }}/>);
190
    };
191

192

193
    renderMembersTab = () => {
16✔
194
        let availableUsers = this.props.availableUsers.filter((user) => findIndex(this.getCurrentGroupMembers(), member => member.id === user.id) < 0).map(u => ({ value: u.id, label: u.name }));
22✔
195
        const pagination = {
22✔
196
            firstPage: this.selectMemberPage === 0,
197
            lastPage: this.isLastPage(),
198
            loadNextPage: this.loadNextPageMembers,
199
            loadPrevPage: this.loadPrevPageMembers,
200
            paginated: true
201
        };
202
        const placeholder = this.context.intl ? this.context.intl.formatMessage({id: 'usergroups.selectMemberPlaceholder'}) : '';
22!
203
        return (<div style={{ marginTop: "10px" }}>
22✔
204
            <label key="member-label" className="control-label"><Message msgId="usergroups.groupMembers" /></label>
205
            <div key="member-list" style={
206
                {
207
                    maxHeight: "200px",
208
                    padding: "5px",
209
                    overflow: "auto",
210
                    boxShadow: "inset 0 2px 5px 0 #AAA"
211
                }} >{this.renderMembers()}</div>
212
            <div key="add-member" style={{ marginTop: "10px" }}>
213
                <label key="add-member-label" className="control-label"><Message msgId="usergroups.addMember" /></label>
214
                <PagedCombobox
215
                    busy={this.props.availableUsersLoading}
216
                    data={availableUsers}
217
                    open={this.state.openSelectMember}
218
                    onToggle={this.handleToggleSelectMember}
219
                    onChange={this.handleSelectMemberOnChange}
220
                    placeholder={placeholder}
221
                    pagination={pagination}
222
                    selectedValue={this.state.selectedMember}
223
                    onSelect={this.handleSelect}
224
                    stopPropagation
225
                />
226
            </div>
227
        </div>);
228
    };
229
    renderAttributes = () => {
16✔
230
        const byName = attrName => ({name}) => attrName === name;
24✔
231
        const group = this.props.group;
22✔
232
        const attributes = group?.attributes ?? [];
22✔
233
        return <>
22✔
234
            {
235
                this.props.attributeFields.map( ({name, title, controlType, ...rest}) => {
236
                    const values = attributes.filter(byName(name)).map(({value}) => value);
24✔
237
                    const value =  values.length === 1 ? values?.[0] : values;
24✔
238
                    const Control = controls[controlType ?? "string"];
24✔
239
                    return (<div style={{ marginTop: "10px" }}>
24✔
240
                        <label key="member-label" className="control-label">{title ?? name}</label>
48✔
241
                        <Control name={name} {...rest} value={value} onChange={(newValue) => {
242
                            // newValue can be an array or a single value to support multiple attributes of the same name.
243
                            const newAttributes = attributes.filter(v => !byName(name)(v)).concat(castArray(newValue).map(vv => ({name, value: vv})));
×
244
                            this.props.onChange("attributes", newAttributes);
×
245
                        }} />
246
                    </div>);
247
                })
248
            }
249
        </>;
250
    };
251

252
    render() {
253
        return (<Dialog
22✔
254
            modal
255
            maskLoading={this.props.group && (this.props.group.status === "loading" || this.props.group.status === "saving")}
65✔
256
            id="mapstore-group-dialog"
257
            className="group-edit-dialog"
258
            style={assign({}, this.props.style, {display: this.props.show ? "block" : "none"})}
22✔
259
            draggable={false}
260
        >
261
            <span role="header">
262
                <button onClick={this.props.onClose} className="login-panel-close close">
263
                    {this.props.closeGlyph ? <Glyphicon glyph={this.props.closeGlyph}/> : <span><Glyphicon glyph="1-close"/></span>}
22!
264
                </button>
265
                <span className="user-panel-title">{(this.props.group && this.props.group.groupName) || <Message msgId="usergroups.newGroup" />}</span>
44!
266
            </span>
267
            <div role="body">
268
                <Tabs justified defaultActiveKey={1} onSelect={ ( key) => { this.setState({key}); }} key="tab-panel">
2✔
269
                    <Tab eventKey={1} title={<Glyphicon glyph="1-group" style={{ display: 'block', padding: 8 }} />} >
270
                        {this.renderGeneral()}
271
                        {this.checkNameLenght()}
272
                        {this.checkDescLenght()}
273
                    </Tab>
274
                    {this.props.showMembersTab ?
22!
275
                        <Tab eventKey={2} title={<Glyphicon glyph="1-group-add" style={{ display: 'block', padding: 8 }} />} >
276
                            {this.renderMembersTab()}
277
                        </Tab>
278
                        : null
279
                    }
280
                    {this.props.showAttributesTab
22!
281
                        ? <Tab eventKey={3} title={<Glyphicon glyph="list-alt" style={{ display: 'block', padding: 8 }} />} >
282
                            {this.renderAttributes()}
283
                        </Tab>
284
                        : null
285
                    }
286
                </Tabs>
287
            </div>
288
            <div role="footer">
289
                {this.renderError()}
290
                {this.renderButtons()}
291
            </div>
292
        </Dialog>);
293
    }
294

295
    // check if pagination last page
296
    isLastPage = () => {
16✔
297
        return (this.selectMemberPage + PAGINATION_LIMIT) > this.props.availableUsersCount;
23✔
298
    }
299

300
    // called before onChange
301
    handleSelect = () => {
16✔
302
        this.selected = true;
1✔
303
    };
304

305
    handleToggleSelectMember = () => {
16✔
306
        this.setState(prevState => ({
2✔
307
            openSelectMember: !prevState.openSelectMember
308
        }));
309
    }
310

311
    handleSelectMemberOnChange = (selected) => {
16✔
312
        if (typeof selected === 'string') {
3✔
313
            this.selectMemberPage = 0;
2✔
314
            this.setState({selectedMember: selected});
2✔
315
            this.searchUsers(selected, true);
2✔
316
            return;
2✔
317
        }
318

319
        if (this.selected) {
1!
320
            this.selected = false;
1✔
321
            let value = selected.value;
1✔
322
            let newMemberIndex = findIndex(this.props.availableUsers, u => u.id === value);
1✔
323
            if (newMemberIndex >= 0) {
1!
324
                let newMember = this.props.availableUsers[newMemberIndex];
1✔
325
                let newUsers = this.getCurrentGroupMembers();
1✔
326
                newUsers = [...newUsers, newMember];
1✔
327
                this.props.onChange("newUsers", newUsers);
1✔
328
                this.setState({selectedMember: '', openSelectMember: false});
1✔
329
                this.searchUsers('*', true);
1✔
330
            }
331
            return;
1✔
332
        }
333
        if (selected.value) {
×
334
            this.setState({selectedMember: selected.label});
×
335
            this.selectMemberPage = 0;
×
336
        }
337
    }
338

339
    loadNextPageMembers = () => {
16✔
340
        if (this.selectMemberPage === 0) {
2✔
341
            this.selectMemberPage = this.selectMemberPage + PAGINATION_LIMIT + 1;
1✔
342
        } else {
343
            this.selectMemberPage = this.selectMemberPage + PAGINATION_LIMIT;
1✔
344
        }
345
        this.searchUsers();
2✔
346
    }
347

348
    loadPrevPageMembers = () => {
16✔
349
        this.selectMemberPage = this.selectMemberPage - PAGINATION_LIMIT;
1✔
350
        if (this.selectMemberPage === 1) {
1!
351
            this.selectMemberPage = 0;
×
352
        }
353
        this.searchUsers();
1✔
354
    }
355

356
    searchUsers = (q, textChanged) => {
16✔
357
        const start = this.selectMemberPage;
22✔
358
        const text = textChanged ? q : (typeof this.state.selectedMember === 'string' && this.state.selectedMember ? this.state.selectedMember : q);
22!
359
        this.props.searchUsers(text, start, PAGINATION_LIMIT);
22✔
360
    }
361

362
    checkNameLenght = () => {
16✔
363
        return this.props.group && this.props.group.groupName && this.props.group.groupName.length === this.props.nameLimit ? <div className="alert alert-warning">
22!
364
            <Message msgId="usergroups.nameLimit"/>
365
        </div> : null;
366
    };
367

368
    checkDescLenght = () => {
16✔
369
        return this.props.group && this.props.group.description && this.props.group.description.length === this.props.descLimit ? <div className="alert alert-warning">
22!
370
            <Message msgId="usergroups.descLimit"/>
371
        </div> : null;
372
    };
373

374
    isSaving = () => {
16✔
375
        return this.props.group && this.props.group.status === "saving";
23✔
376
    };
377

378
    isSaved = () => {
16✔
379
        return this.props.group && (this.props.group.status === "saved" || this.props.group.status === "created");
22✔
380
    };
381

382
    isValid = () => {
16✔
383
        let valid = true;
22✔
384
        let group = this.props.group;
22✔
385
        if (!group) return false;
22!
386
        valid = valid && group.groupName && group.status === "modified";
22✔
387
        return valid;
22✔
388
    };
389

390
    handleChange = (event) => {
16✔
391
        this.props.onChange(event.target.name, event.target.value);
×
392
    };
393
}
394

395
export default GroupDialog;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc