Skip to content

Commit 46516f1

Browse files
committed
Add control to enable pre-moderation in groups
1 parent a5f1dc6 commit 46516f1

File tree

7 files changed

+107
-12
lines changed

7 files changed

+107
-12
lines changed

Diff for: h/static/scripts/group-forms/components/CreateEditGroupForm.tsx

+50-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
LockFilledIcon,
77
GlobeIcon,
88
GlobeLockIcon,
9+
CheckboxCheckedFilledIcon,
10+
CheckboxIcon,
911
} from '@hypothesis/frontend-shared';
1012
import { Config } from '../config';
1113
import type { Group } from '../config';
@@ -26,6 +28,7 @@ import TextField from './forms/TextField';
2628
import GroupFormHeader from './GroupFormHeader';
2729
import SaveStateIcon from './SaveStateIcon';
2830
import WarningDialog from './WarningDialog';
31+
import classnames from 'classnames';
2932

3033
/**
3134
* Dialog that warns users about existing annotations in a group being exposed
@@ -108,6 +111,9 @@ export default function CreateEditGroupForm({
108111
const [groupType, setGroupType] = useState<GroupType>(
109112
group?.type ?? 'private',
110113
);
114+
const [moderationEnabled, setModerationEnabled] = useState(
115+
group?.moderation_enabled ?? false,
116+
);
111117

112118
// Set when the user selects a new group type if confirmation is required.
113119
// Cleared after confirmation.
@@ -158,6 +164,7 @@ export default function CreateEditGroupForm({
158164
name,
159165
description,
160166
type: groupType,
167+
moderation_enabled: moderationEnabled,
161168
};
162169

163170
response = (await callAPI(config.api.createGroup.url, {
@@ -185,6 +192,7 @@ export default function CreateEditGroupForm({
185192
name,
186193
description,
187194
type: groupType,
195+
moderation_enabled: moderationEnabled,
188196
};
189197

190198
(await callAPI(config.api.updateGroup!.url, {
@@ -241,7 +249,11 @@ export default function CreateEditGroupForm({
241249
title={heading}
242250
enableMembers={config.features.group_members}
243251
/>
244-
<form onSubmit={onSubmit} data-testid="form">
252+
<form
253+
onSubmit={onSubmit}
254+
data-testid="form"
255+
className="flex flex-col gap-y-4"
256+
>
245257
<TextField
246258
type="input"
247259
value={name}
@@ -262,7 +274,7 @@ export default function CreateEditGroupForm({
262274
/>
263275

264276
{config.features.group_type && (
265-
<>
277+
<div>
266278
<Label id={groupTypeLabel} text="Group type" />
267279
<RadioGroup
268280
aria-labelledby={groupTypeLabel}
@@ -290,10 +302,44 @@ export default function CreateEditGroupForm({
290302
<GlobeIcon /> Open
291303
</RadioGroup.Radio>
292304
</RadioGroup>
293-
</>
305+
</div>
306+
)}
307+
308+
{config.features.group_moderation && (
309+
<div>
310+
<Label text="Pre-moderation" />
311+
<div
312+
className={classnames(
313+
'focus-visible-ring flex gap-x-1.5 items-start',
314+
'px-3 py-2 rounded-lg cursor-pointer',
315+
'aria-checked:bg-grey-3/50',
316+
)}
317+
role="checkbox"
318+
aria-checked={moderationEnabled}
319+
onClick={() => setModerationEnabled(prev => !prev)}
320+
onKeyDown={e =>
321+
['Enter', ' '].includes(e.key) &&
322+
setModerationEnabled(prev => !prev)
323+
}
324+
tabIndex={0}
325+
data-testid="pre-moderation"
326+
>
327+
{!moderationEnabled && <CheckboxIcon className="mt-1" />}
328+
{moderationEnabled && (
329+
<CheckboxCheckedFilledIcon className="mt-1" />
330+
)}
331+
<div>
332+
<p>Enable pre-moderation for this group</p>
333+
<p>
334+
Moderators must approve new annotations before they are shown
335+
to group members.
336+
</p>
337+
</div>
338+
</div>
339+
</div>
294340
)}
295341

296-
<div className="mt-2 pt-2 border-t border-t-text-grey-6 flex items-center gap-x-4">
342+
<div className="pt-2 border-t border-t-text-grey-6 flex items-center gap-x-4">
297343
<span>
298344
{/* These are in a child span to avoid a gap between them. */}
299345
<Star />

Diff for: h/static/scripts/group-forms/components/forms/TextField.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export default function TextField({
9898
const InputComponent = type === 'input' ? Input : Textarea;
9999

100100
return (
101-
<div className="mb-4">
101+
<div>
102102
<Label htmlFor={id} text={label} required={required} />
103103
<InputComponent
104104
id={id}

Diff for: h/static/scripts/group-forms/components/test/CreateEditGroupForm-test.js

+41
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ describe('CreateEditGroupForm', () => {
196196
name,
197197
description,
198198
type,
199+
moderation_enabled: false,
199200
},
200201
});
201202
assert.calledOnceWithExactly(fakeSetLocation, groupURL);
@@ -219,6 +220,45 @@ describe('CreateEditGroupForm', () => {
219220
assert.isFalse(savedConfirmationShowing(wrapper));
220221
});
221222

223+
describe('pre-moderation', () => {
224+
const isModerationEnabled = wrapper =>
225+
wrapper.find('[data-testid="pre-moderation"]').prop('aria-checked');
226+
227+
[true, false].forEach(moderationEnabled => {
228+
it('shows pre-moderation checkbox when group moderation is enabled', () => {
229+
config.features.group_moderation = moderationEnabled;
230+
const { wrapper } = createWrapper();
231+
232+
assert.equal(
233+
wrapper.exists('[data-testid="pre-moderation"]'),
234+
moderationEnabled,
235+
);
236+
});
237+
});
238+
239+
it('toggles pre-moderation by clicking on it', () => {
240+
config.features.group_moderation = true;
241+
const { wrapper } = createWrapper();
242+
243+
assert.isFalse(isModerationEnabled(wrapper));
244+
wrapper.find('[data-testid="pre-moderation"]').simulate('click');
245+
assert.isTrue(isModerationEnabled(wrapper));
246+
});
247+
248+
['Enter', ' '].forEach(key => {
249+
it('toggles pre-moderation by pressing Enter or Space', () => {
250+
config.features.group_moderation = true;
251+
const { wrapper } = createWrapper();
252+
253+
assert.isFalse(isModerationEnabled(wrapper));
254+
wrapper
255+
.find('[data-testid="pre-moderation"]')
256+
.simulate('keydown', { key });
257+
assert.isTrue(isModerationEnabled(wrapper));
258+
});
259+
});
260+
});
261+
222262
context('when editing an existing group', () => {
223263
beforeEach(() => {
224264
config.context.group = {
@@ -329,6 +369,7 @@ describe('CreateEditGroupForm', () => {
329369
name,
330370
description,
331371
type: newGroupType,
372+
moderation_enabled: false,
332373
},
333374
}),
334375
);

Diff for: h/static/scripts/group-forms/config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type Group = {
1414
link: string;
1515
type: GroupType;
1616
num_annotations: number;
17+
moderation_enabled?: boolean;
1718
};
1819

1920
export type ConfigObject = {
@@ -35,6 +36,7 @@ export type ConfigObject = {
3536
features: {
3637
group_members: boolean;
3738
group_type: boolean;
39+
group_moderation: boolean;
3840
};
3941
};
4042

Diff for: h/static/scripts/group-forms/utils/api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type CreateUpdateGroupAPIRequest = {
1919
name: string;
2020
description?: string;
2121
type?: GroupType;
22+
moderation_enabled?: boolean;
2223
};
2324

2425
/**

Diff for: h/views/groups.py

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def api_config(route_name: str, method: str, **kw):
7070
"features": {
7171
"group_members": self.request.feature("group_members"),
7272
"group_type": self.request.feature("group_type"),
73+
"group_moderation": self.request.feature("group_moderation"),
7374
},
7475
}
7576

Diff for: tests/unit/h/views/groups_test.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010

1111
@pytest.mark.usefixtures("annotation_stats_service")
1212
class TestGroupCreateEditController:
13-
@pytest.mark.parametrize("group_type_flag", [True, False])
14-
def test_create(self, pyramid_request, assets_env, mocker, group_type_flag):
15-
pyramid_request.feature.flags["group_type"] = group_type_flag
13+
@pytest.mark.parametrize("flag", [True, False])
14+
def test_create(self, pyramid_request, assets_env, mocker, flag):
15+
pyramid_request.feature.flags["group_type"] = flag
16+
pyramid_request.feature.flags["group_moderation"] = flag
1617

1718
mocker.spy(views, "get_csrf_token")
1819

@@ -24,9 +25,7 @@ def test_create(self, pyramid_request, assets_env, mocker, group_type_flag):
2425
views.get_csrf_token.assert_called_once_with(pyramid_request)
2526
assert result == {
2627
"page_title": (
27-
"Create a new group"
28-
if group_type_flag
29-
else "Create a new private group"
28+
"Create a new group" if flag else "Create a new private group"
3029
),
3130
"js_config": {
3231
"styles": assets_env.urls.return_value,
@@ -42,8 +41,9 @@ def test_create(self, pyramid_request, assets_env, mocker, group_type_flag):
4241
"user": {"userid": sentinel.authenticated_userid},
4342
},
4443
"features": {
45-
"group_type": group_type_flag,
44+
"group_type": flag,
4645
"group_members": pyramid_request.feature.flags["group_members"],
46+
"group_moderation": flag,
4747
},
4848
},
4949
}
@@ -117,6 +117,9 @@ def test_edit(
117117
"features": {
118118
"group_type": pyramid_request.feature.flags["group_type"],
119119
"group_members": pyramid_request.feature.flags["group_members"],
120+
"group_moderation": pyramid_request.feature.flags[
121+
"group_moderation"
122+
],
120123
},
121124
},
122125
}
@@ -135,6 +138,7 @@ def pyramid_config(self, pyramid_config, assets_env):
135138
def pyramid_request(self, pyramid_request):
136139
pyramid_request.feature.flags["group_type"] = True
137140
pyramid_request.feature.flags["group_members"] = True
141+
pyramid_request.feature.flags["group_moderation"] = True
138142
return pyramid_request
139143

140144

0 commit comments

Comments
 (0)