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

geosolutions-it / MapStore2 / 14399216359

11 Apr 2025 08:42AM UTC coverage: 76.985% (-0.02%) from 77.006%
14399216359

Pull #10973

github

web-flow
Merge d87ab24be into 370d48ef2
Pull Request #10973: Fix #10820 Refactor Admin Ui section like HomePage using ResourceGrid Plugin.

30798 of 47937 branches covered (64.25%)

53 of 54 new or added lines in 8 files covered. (98.15%)

4 existing lines in 3 files now uncovered.

38284 of 49729 relevant lines covered (76.99%)

36.28 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 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
    handleSaveGroup = () =>{
16✔
NEW
156
        this.props.onSave(this.props.group);
×
157
    }
158

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

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

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

196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

399
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