Skip to content

Commit acfc460

Browse files
committed
Show warning dialog when changing group type
Warn the user if changing the group type may expose or hide annotations that are already in the group from public view.
1 parent 4d6f1f7 commit acfc460

3 files changed

Lines changed: 183 additions & 2 deletions

File tree

h/static/scripts/group-forms/components/CreateEditGroupForm.tsx

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Input,
77
RadioGroup,
88
Textarea,
9+
ModalDialog,
910
useWarnOnPageUnload,
1011
} from '@hypothesis/frontend-shared';
1112
import { readConfig } from '../config';
@@ -17,6 +18,7 @@ import type {
1718
} from '../utils/api';
1819
import { setLocation } from '../utils/set-location';
1920
import SaveStateIcon from './SaveStateIcon';
21+
import WarningDialog from './WarningDialog';
2022

2123
function Star() {
2224
return <span className="text-brand">*</span>;
@@ -133,6 +135,58 @@ function TextField({
133135
);
134136
}
135137

138+
/**
139+
* Dialog that warns users about existing annotations in a group being exposed
140+
* or hidden from public view when the group type is changed.
141+
*/
142+
function GroupTypeChangeWarning({
143+
name,
144+
newType,
145+
annotationCount: count,
146+
onConfirm,
147+
onCancel,
148+
}: {
149+
/** Name of the group. */
150+
name: string;
151+
152+
/**
153+
* The new type for the group. If this is private, the old type is inferred
154+
* to be public and vice-versa.
155+
*/
156+
newType: GroupType;
157+
158+
/** Number of annotations in the group. */
159+
annotationCount: number;
160+
161+
onConfirm: () => void;
162+
onCancel: () => void;
163+
}) {
164+
const newTypeIsPrivate = newType === 'private';
165+
166+
let title;
167+
let confirmAction;
168+
let message;
169+
if (newTypeIsPrivate) {
170+
title = `Make ${count} annotations private?`;
171+
confirmAction = 'Make annotations private';
172+
message = `Are you sure you want to make "${name}" private? ${count} annotations that are publicly visible will become visible only to members of ${name}`;
173+
} else {
174+
title = `Make ${count} annotations public?`;
175+
confirmAction = 'Make annotations public';
176+
message = `Are you sure you want to make "${name}" public? ${count} annotations that are visible only to group members will become publicly visible`;
177+
}
178+
179+
return (
180+
<WarningDialog
181+
title={title}
182+
message={message}
183+
confirmAction={confirmAction}
184+
onConfirm={onConfirm}
185+
onCancel={onCancel}
186+
/>
187+
);
188+
}
189+
136190
export default function CreateEditGroupForm() {
137191
const config = useMemo(() => readConfig(), []);
138192
const group = config.context.group;
@@ -143,6 +197,12 @@ export default function CreateEditGroupForm() {
143197
group?.type ?? 'private',
144198
);
145199

200+
// Set when the user selects a new group type if confirmation is required.
201+
// Cleared after confirmation.
202+
const [pendingGroupType, setPendingGroupType] = useState<GroupType | null>(
203+
null,
204+
);
205+
146206
const [errorMessage, setErrorMessage] = useState('');
147207
const [saveState, setSaveState] = useState<
148208
'unmodified' | 'unsaved' | 'saving' | 'saved'
@@ -249,6 +309,18 @@ export default function CreateEditGroupForm() {
249309

250310
const groupTypeLabel = useId();
251311

312+
const changeGroupType = (newType: GroupType) => {
313+
const count = group?.num_annotations ?? 0;
314+
const oldTypeIsPrivate = groupType === 'private';
315+
const newTypeIsPrivate = newType === 'private';
316+
317+
if (count === 0 || oldTypeIsPrivate === newTypeIsPrivate) {
318+
setGroupType(newType);
319+
} else {
320+
setPendingGroupType(newType);
321+
}
322+
};
323+
252324
return (
253325
<div className="text-grey-6 text-sm/relaxed">
254326
<h1 className="mt-14 mb-8 text-grey-7 text-xl/none" data-testid="header">
@@ -280,12 +352,12 @@ export default function CreateEditGroupForm() {
280352
{config.features.group_type && (
281353
<>
282354
<Label id={groupTypeLabel} text="Group type" />
283-
<RadioGroup<GroupType>
355+
<RadioGroup
284356
aria-labelledby={groupTypeLabel}
285357
data-testid="group-type"
286358
direction="vertical"
287359
selected={groupType}
288-
onChange={setGroupType}
360+
onChange={changeGroupType}
289361
>
290362
<RadioGroup.Radio
291363
value="private"
@@ -336,6 +408,19 @@ export default function CreateEditGroupForm() {
336408
</div>
337409
</form>
338410

411+
{group && pendingGroupType && (
412+
<GroupTypeChangeWarning
413+
name={name}
414+
newType={pendingGroupType}
415+
annotationCount={group.num_annotations}
416+
onCancel={() => setPendingGroupType(null)}
417+
onConfirm={() => {
418+
setGroupType(pendingGroupType);
419+
setPendingGroupType(null);
420+
}}
421+
/>
422+
)}
423+
339424
<footer className="mt-14 pt-4 border-t border-t-text-grey-6">
340425
<div className="flex">
341426
{group && (

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,101 @@ describe('CreateEditGroupForm', () => {
477477
});
478478
});
479479
});
480+
481+
[
482+
// There no warning when creating a new group
483+
{
484+
oldType: null,
485+
newType: 'open',
486+
expectedWarning: null,
487+
annotationCount: 0,
488+
},
489+
{
490+
// Warn when making annotations public
491+
oldType: 'private',
492+
newType: 'open',
493+
annotationCount: 2,
494+
expectedWarning: 'Make 2 annotations public?',
495+
},
496+
{
497+
// Warn when making annotations private
498+
oldType: 'open',
499+
newType: 'private',
500+
annotationCount: 3,
501+
expectedWarning: 'Make 3 annotations private?',
502+
},
503+
{
504+
// Don't warn if there are no annotations
505+
oldType: 'open',
506+
newType: 'private',
507+
annotationCount: 0,
508+
expectedWarning: null,
509+
},
510+
].forEach(({ oldType, newType, annotationCount, expectedWarning }) => {
511+
it('shows warning when changing group type between private and public', async () => {
512+
if (oldType !== null) {
513+
config.context.group = {
514+
pubid: 'testid',
515+
name: 'Test Name',
516+
description: 'Test group description',
517+
link: 'https://example.com/groups/testid',
518+
type: oldType,
519+
num_annotations: annotationCount,
520+
};
521+
}
522+
523+
const { wrapper } = createWrapper();
524+
setSelectedGroupType(wrapper, newType);
525+
526+
if (expectedWarning) {
527+
const warning = wrapper.find('WarningDialog');
528+
assert.isTrue(warning.exists());
529+
assert.equal(warning.prop('title'), expectedWarning);
530+
} else {
531+
assert.isFalse(wrapper.exists('WarningDialog'));
532+
}
533+
});
534+
});
535+
536+
it('updates group type if change is confirmed', async () => {
537+
config.context.group = {
538+
pubid: 'testid',
539+
name: 'Test Name',
540+
description: 'Test group description',
541+
link: 'https://example.com/groups/testid',
542+
type: 'private',
543+
num_annotations: 3,
544+
};
545+
546+
const { wrapper } = createWrapper();
547+
setSelectedGroupType(wrapper, 'open');
548+
549+
const warning = wrapper.find('WarningDialog');
550+
act(() => warning.prop('onConfirm')());
551+
wrapper.update();
552+
553+
assert.equal(getSelectedGroupType(wrapper), 'open');
554+
});
555+
556+
it('does not group type if change is canceled', async () => {
557+
config.context.group = {
558+
pubid: 'testid',
559+
name: 'Test Name',
560+
description: 'Test group description',
561+
link: 'https://example.com/groups/testid',
562+
type: 'private',
563+
num_annotations: 3,
564+
};
565+
566+
const { wrapper } = createWrapper();
567+
setSelectedGroupType(wrapper, 'open');
568+
569+
const warning = wrapper.find('WarningDialog');
570+
act(() => warning.prop('onCancel')());
571+
wrapper.update();
572+
573+
assert.equal(getSelectedGroupType(wrapper), 'private');
574+
});
480575
});
481576

482577
async function assertInLoadingState(wrapper, inLoadingState) {

h/static/scripts/group-forms/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type ConfigObject = {
2020
description: string;
2121
link: string;
2222
type: GroupType;
23+
num_annotations: number;
2324
} | null;
2425
};
2526
features: {

0 commit comments

Comments
 (0)