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

geosolutions-it / MapStore2 / 15829819958

23 Jun 2025 04:29PM UTC coverage: 76.979% (+0.03%) from 76.95%
15829819958

Pull #11183

github

web-flow
Merge 124f321fe into 7cde38ac9
Pull Request #11183: #11165: Option to deny app context for normal users

31124 of 48441 branches covered (64.25%)

14 of 16 new or added lines in 5 files covered. (87.5%)

1401 existing lines in 128 files now uncovered.

38752 of 50341 relevant lines covered (76.98%)

36.22 hits per line

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

90.0
/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 Spinner from 'react-spinkit';
15
import { findIndex, castArray } from 'lodash';
16

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

25
import controls from './AttributeControls';
26

27

28
const PAGINATION_LIMIT = 5;
1✔
29

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

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

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

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

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

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

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

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

154
    handleSaveGroup = () =>{
16✔
UNCOV
155
        this.props.onSave(this.props.group);
×
156
    }
157

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

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

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

195

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

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

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

303
    // called before onChange
304
    handleSelect = () => {
16✔
305
        this.selected = true;
1✔
306
    };
307

308
    handleToggleSelectMember = () => {
16✔
309
        this.setState(prevState => ({
2✔
310
            openSelectMember: !prevState.openSelectMember
311
        }));
312
    }
313

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

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

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

351
    loadPrevPageMembers = () => {
16✔
352
        this.selectMemberPage = this.selectMemberPage - PAGINATION_LIMIT;
1✔
353
        if (this.selectMemberPage === 1) {
1!
UNCOV
354
            this.selectMemberPage = 0;
×
355
        }
356
        this.searchUsers();
1✔
357
    }
358

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

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

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

377
    isSaving = () => {
16✔
378
        return this.props.group && this.props.group.status === "saving";
23✔
379
    };
380

381
    isSaved = () => {
16✔
382
        return this.props.group && (this.props.group.status === "saved" || this.props.group.status === "created");
22✔
383
    };
384

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

393
    handleChange = (event) => {
16✔
UNCOV
394
        this.props.onChange(event.target.name, event.target.value);
×
395
    };
396
}
397

398
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