Skip to content

Commit 7cb3342

Browse files
authored
Merge pull request #2649 from objectcomputing/setting-option-values
Setting categories are not grouped properly
2 parents 437283b + 5717e70 commit 7cb3342

File tree

9 files changed

+295
-36
lines changed

9 files changed

+295
-36
lines changed

server/src/main/java/com/objectcomputing/checkins/services/settings/SettingOption.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,26 @@
1616
@JsonDeserialize(using = SettingOptionDeserializer.class)
1717
public enum SettingOption {
1818
LOGO_URL("The logo url", Category.THEME, Type.FILE),
19-
PULSE_EMAIL_FREQUENCY("The Pulse Email Frequency (weekly, bi-weekly, monthly)", Category.CHECK_INS, Type.STRING);
19+
PULSE_EMAIL_FREQUENCY("The Pulse Email Frequency", Category.CHECK_INS, Type.STRING, List.of("weekly", "bi-weekly", "monthly"));
2020

2121
private final String description;
2222
private final Category category;
2323
private final Type type;
24+
private final List<String> values;
2425

2526
SettingOption(String description, Category category, Type type) {
2627
this.description = description;
2728
this.category = category;
2829
this.type = type;
30+
this.values = List.of();
2931
}
3032

31-
public String getDescription() {
32-
return description;
33-
}
34-
35-
public Category getCategory() {
36-
return category;
37-
}
38-
39-
public Type getType() {
40-
return type;
33+
SettingOption(String description, Category category, Type type,
34+
List<String> values) {
35+
this.description = description;
36+
this.category = category;
37+
this.type = type;
38+
this.values = values;
4139
}
4240

4341
public static List<SettingOption> getOptions(){

server/src/main/java/com/objectcomputing/checkins/services/settings/SettingOptionSerializer.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
66

77
import java.io.IOException;
8+
import java.util.List;
89

910
public class SettingOptionSerializer extends StdSerializer<SettingOption> {
1011

@@ -28,6 +29,15 @@ public void serialize(
2829
generator.writeString(settingOption.getCategory().name());
2930
generator.writeFieldName("type");
3031
generator.writeString(settingOption.getType().name());
32+
List<String> values = settingOption.getValues();
33+
if (!values.isEmpty()) {
34+
generator.writeFieldName("values");
35+
generator.writeStartArray(values.size());
36+
for(String value : values) {
37+
generator.writeString(value);
38+
}
39+
generator.writeEndArray();
40+
}
3141
generator.writeEndObject();
3242
}
33-
}
43+
}

server/src/main/java/com/objectcomputing/checkins/services/settings/SettingsController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,8 @@ public SettingsResponseDTO findByName(@PathVariable @NotNull String name) {
7676
public List<SettingsResponseDTO> getOptions() {
7777
List<SettingOption> options = SettingOption.getOptions();
7878
return options.stream().map(option -> {
79-
// Default to an empty value and "invalid" UUID.
80-
// This can be used by the client to determine pre-existance.
8179
String value = "";
82-
UUID uuid = new UUID(0, 0);
80+
UUID uuid = null;
8381
try {
8482
Setting s = settingsServices.findByName(option.name());
8583
uuid = s.getId();
@@ -89,6 +87,7 @@ public List<SettingsResponseDTO> getOptions() {
8987
return new SettingsResponseDTO(
9088
uuid, option.name(), option.getDescription(),
9189
option.getCategory(), option.getType(),
90+
option.getValues(),
9291
value);
9392
}).toList();
9493
}
@@ -150,6 +149,7 @@ private SettingsResponseDTO fromEntity(Setting entity) {
150149
dto.setDescription(option.getDescription());
151150
dto.setCategory(option.getCategory());
152151
dto.setType(option.getType());
152+
dto.setValues(option.getValues());
153153
dto.setValue(entity.getValue());
154154
return dto;
155155
}

server/src/main/java/com/objectcomputing/checkins/services/settings/SettingsResponseDTO.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import lombok.Setter;
1010

1111
import java.util.UUID;
12+
import java.util.List;
1213

1314
@Setter
1415
@Getter
@@ -36,6 +37,10 @@ public class SettingsResponseDTO {
3637
@Schema(description = "type of the setting")
3738
private SettingOption.Type type;
3839

40+
@NotNull
41+
@Schema(description = "possible values for the setting")
42+
private List<String> values;
43+
3944
@NotBlank
4045
@Schema(description = "value of the setting")
4146
private String value;

web-ui/src/components/settings/types/number.jsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import React from 'react';
2-
import { Input, Typography } from '@mui/material';
2+
import {
3+
Select,
4+
MenuItem,
5+
ListItemText,
6+
Input,
7+
Typography
8+
} from '@mui/material';
39
import { createLabelId } from '../../../helpers/strings.js';
410

511
/**
@@ -13,7 +19,7 @@ import { createLabelId } from '../../../helpers/strings.js';
1319
* @param {function} props.handleChange - The callback function to handle value changes.
1420
* @returns {JSX.Element} - The rendered component.
1521
*/
16-
const SettingsNumber = ({ name, description, value, handleChange }) => {
22+
const SettingsNumber = ({ name, description, values, value, handleChange }) => {
1723
const labelId = createLabelId(name);
1824

1925
return (
@@ -24,13 +30,25 @@ const SettingsNumber = ({ name, description, value, handleChange }) => {
2430
</Typography>
2531
</label>
2632
{description && <p>{description}</p>}
33+
{values && values.length > 0 ?
34+
<Select
35+
labelId={labelId}
36+
value={value}
37+
onChange={handleChange}
38+
>
39+
{values.map((option) => (
40+
<MenuItem key={option} value={option}>
41+
<ListItemText primary={option} />
42+
</MenuItem>
43+
))}
44+
</Select> :
2745
<Input
2846
id={labelId}
2947
className="settings-control"
3048
type="number"
3149
value={value}
3250
onChange={handleChange}
33-
/>
51+
/>}
3452
</div>
3553
);
3654
};

web-ui/src/components/settings/types/string.jsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import React from 'react';
2-
import { Input, Typography } from '@mui/material';
2+
import {
3+
Select,
4+
MenuItem,
5+
ListItemText,
6+
Input,
7+
Typography
8+
} from '@mui/material';
39
import { createLabelId } from '../../../helpers/strings.js';
410

511
/**
@@ -17,6 +23,7 @@ import { createLabelId } from '../../../helpers/strings.js';
1723
const SettingsString = ({
1824
name,
1925
description,
26+
values,
2027
value,
2128
placeholder,
2229
handleChange
@@ -31,14 +38,26 @@ const SettingsString = ({
3138
</Typography>
3239
</label>
3340
{description && <p>{description}</p>}
41+
{values && values.length > 0 ?
42+
<Select
43+
labelId={labelId}
44+
value={value}
45+
onChange={handleChange}
46+
>
47+
{values.map((option) => (
48+
<MenuItem key={option} value={option}>
49+
<ListItemText primary={option} />
50+
</MenuItem>
51+
))}
52+
</Select> :
3453
<Input
3554
id={labelId}
3655
className="settings-control"
3756
type="text"
3857
value={value}
3958
placeholder={placeholder ?? `Enter ${name}`}
4059
onChange={handleChange}
41-
/>
60+
/>}
4261
</div>
4362
);
4463
};

web-ui/src/pages/SettingsPage.jsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,15 @@ const SettingsPage = () => {
4444
(await getAllOptions()).payload.data : [];
4545

4646
if (allOptions) {
47-
// If the option has a valid UUID, then the setting already exists.
48-
// This information is necessary to know since we must use POST to
49-
// create new settings and PUT to modify existing settings.
50-
for (let option of allOptions) {
51-
option.exists = option?.id != '00000000-0000-0000-0000-000000000000';
52-
}
47+
// Sort the options by category, store them, and upate the state.
48+
setSettingsControls(
49+
allOptions.sort((l, r) => l.category.localeCompare(r.category)));
5350
}
54-
55-
// Sort the options by category, store them, and upate the state.
56-
setSettingsControls(
57-
allOptions.sort((l, r) => l.category.localeCompare(r.category)));
5851
};
59-
fetchData();
60-
}, []);
52+
if (csrf) {
53+
fetchData();
54+
}
55+
}, [state, csrf]);
6156

6257
// For specific settings, add a handleFunction to the settings object.
6358
// Format should be handleSetting and then add it to the handlers object
@@ -126,12 +121,12 @@ const SettingsPage = () => {
126121
const save = async () => {
127122
let errors;
128123
let saved = 0;
129-
for( let key of Object.keys(handlers)) {
124+
for(let key of Object.keys(handlers)) {
130125
const setting = handlers[key].setting;
131126
// The settings controller does not allow blank values.
132127
if (setting && setting.value) {
133128
let res;
134-
if (setting.exists) {
129+
if (setting.id) {
135130
res = await putOption({ name: setting.name,
136131
value: setting.value }, csrf);
137132
} else {
@@ -183,12 +178,12 @@ const SettingsPage = () => {
183178

184179
/** @type {Controls[]} */
185180
const updatedSettingsControls = addHandlersToSettings(settingsControls);
181+
const categories = {};
186182

187183
return (selectHasViewSettingsPermission(state) ||
188184
selectHasAdministerSettingsPermission(state)) ? (
189185
<div className="settings-page">
190186
{updatedSettingsControls.map((componentInfo, index) => {
191-
const categories = {};
192187
const Component = componentMapping[componentInfo.type.toUpperCase()];
193188
const info = {...componentInfo, name: titleCase(componentInfo.name)};
194189
if (categories[info.category]) {
@@ -197,7 +192,8 @@ const SettingsPage = () => {
197192
categories[info.category] = true;
198193
return (
199194
<>
200-
<Typography variant="h4"
195+
<Typography data-testid={info.category}
196+
variant="h4"
201197
sx={{textDecoration: 'underline'}}
202198
display="inline">{titleCase(info.category)}</Typography>
203199
<Component key={index} {...info} />
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import SettingsPage from './SettingsPage';
3+
import { AppContextProvider } from '../context/AppContext';
4+
import { http, HttpResponse } from 'msw';
5+
import { setupServer } from 'msw/node';
6+
import { BrowserRouter } from 'react-router-dom';
7+
8+
const initialState = {
9+
state: {
10+
csrf: 'csrf',
11+
userProfile: {
12+
name: 'Current User',
13+
role: ['MEMBER'],
14+
permissions: [{ permission: 'CAN_ADMINISTER_SETTINGS' }],
15+
},
16+
loading: {
17+
teams: [],
18+
},
19+
}
20+
};
21+
22+
const server = setupServer(
23+
http.get('http://localhost:8080/services/settings/options', ({ request }) => {
24+
return HttpResponse.json([
25+
{
26+
'name': 'STRING_SETTING',
27+
'description': 'The description',
28+
'category': 'THEME',
29+
'type': 'STRING',
30+
'value': 'The value',
31+
},
32+
{
33+
'name': 'OTHER_SETTING',
34+
'description': 'The description',
35+
'category': 'THEME',
36+
'type': 'NUMBER',
37+
'value': '42',
38+
},
39+
{
40+
'name': 'ANOTHER_SETTING',
41+
'description': 'The description',
42+
'category': 'INTEGRATIONS',
43+
'type': 'BOOLEAN',
44+
'value': 'false',
45+
},
46+
]);
47+
}),
48+
);
49+
50+
beforeAll(() => server.listen());
51+
afterEach(() => server.resetHandlers());
52+
afterAll(() => server.close());
53+
54+
describe('SettingsPage', () => {
55+
it('renders correctly', async () => {
56+
await waitForSnapshot(
57+
// There are two settings with the THEME category. If the page is
58+
// rendered correctly, there will only be one THEME category heading.
59+
'THEME',
60+
<AppContextProvider value={initialState}>
61+
<BrowserRouter>
62+
<SettingsPage />
63+
</BrowserRouter>
64+
</AppContextProvider>
65+
);
66+
});
67+
68+
it('renders an error if user does not have appropriate permission', () => {
69+
snapshot(
70+
<AppContextProvider>
71+
<BrowserRouter>
72+
<SettingsPage />
73+
</BrowserRouter>
74+
</AppContextProvider>
75+
);
76+
});
77+
});

0 commit comments

Comments
 (0)