Skip to content

Commit

Permalink
Show warning dialog when changing group type
Browse files Browse the repository at this point in the history
Warn the user if changing the group type may expose or hide annotations that are
already in the group from public view.
  • Loading branch information
robertknight committed Oct 3, 2024
1 parent 4d6f1f7 commit dbd6892
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 2 deletions.
99 changes: 97 additions & 2 deletions h/static/scripts/group-forms/components/CreateEditGroupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Input,
RadioGroup,
Textarea,
ModalDialog,
useWarnOnPageUnload,
} from '@hypothesis/frontend-shared';
import { readConfig } from '../config';
Expand All @@ -17,6 +18,7 @@ import type {
} from '../utils/api';
import { setLocation } from '../utils/set-location';
import SaveStateIcon from './SaveStateIcon';
import WarningDialog from './WarningDialog';

function Star() {
return <span className="text-brand">*</span>;
Expand Down Expand Up @@ -133,6 +135,68 @@ function TextField({
);
}

/**
* Dialog that warns users about existing annotations in a group being exposed
* or hidden from public view when the group type is changed.
*/
function GroupTypeChangeWarning({
name,
newType,
annotationCount: count,
onConfirm,
onCancel,
}: {
/** Name of the group. */
name: string;

/**
* The new type for the group. If this is private, the old type is inferred
* to be public and vice-versa.
*/
newType: GroupType;

/** Number of annotations in the group. */
annotationCount: number;

onConfirm: () => void;
onCancel: () => void;
}) {
const newTypeIsPrivate = newType === 'private';

let title;
let confirmAction;
let message;
if (newTypeIsPrivate) {
title = `Make ${count} annotations private?`;
confirmAction = 'Make annotations private';
message = `Are you sure you want to make "${name}" a private group? ${count} annotations that are publicly visible will become visible only to members of "${name}".`;
} else {
let groupDescription = 'a public group';
switch (newType) {
case 'open':
groupDescription = 'an open group';
break;
case 'restricted':
groupDescription = 'a restricted group';
break;
}

title = `Make ${count} annotations public?`;
confirmAction = 'Make annotations public';
message = `Are you sure you want to make "${name}" ${groupDescription}? ${count} annotations that are visible only to members of "${name}" will become publicly visible.`;
}

return (
<WarningDialog
title={title}
message={message}
confirmAction={confirmAction}
onConfirm={onConfirm}
onCancel={onCancel}
/>
);
}

export default function CreateEditGroupForm() {
const config = useMemo(() => readConfig(), []);
const group = config.context.group;
Expand All @@ -143,6 +207,12 @@ export default function CreateEditGroupForm() {
group?.type ?? 'private',
);

// Set when the user selects a new group type if confirmation is required.
// Cleared after confirmation.
const [pendingGroupType, setPendingGroupType] = useState<GroupType | null>(
null,
);

const [errorMessage, setErrorMessage] = useState('');
const [saveState, setSaveState] = useState<
'unmodified' | 'unsaved' | 'saving' | 'saved'
Expand Down Expand Up @@ -249,6 +319,18 @@ export default function CreateEditGroupForm() {

const groupTypeLabel = useId();

const changeGroupType = (newType: GroupType) => {
const count = group?.num_annotations ?? 0;
const oldTypeIsPrivate = groupType === 'private';
const newTypeIsPrivate = newType === 'private';

if (count === 0 || oldTypeIsPrivate === newTypeIsPrivate) {
setGroupType(newType);
} else {
setPendingGroupType(newType);
}
};

return (
<div className="text-grey-6 text-sm/relaxed">
<h1 className="mt-14 mb-8 text-grey-7 text-xl/none" data-testid="header">
Expand Down Expand Up @@ -280,12 +362,12 @@ export default function CreateEditGroupForm() {
{config.features.group_type && (
<>
<Label id={groupTypeLabel} text="Group type" />
<RadioGroup<GroupType>
<RadioGroup
aria-labelledby={groupTypeLabel}
data-testid="group-type"
direction="vertical"
selected={groupType}
onChange={setGroupType}
onChange={changeGroupType}
>
<RadioGroup.Radio
value="private"
Expand Down Expand Up @@ -336,6 +418,19 @@ export default function CreateEditGroupForm() {
</div>
</form>

{group && pendingGroupType && (
<GroupTypeChangeWarning
name={name}
newType={pendingGroupType}
annotationCount={group.num_annotations}
onCancel={() => setPendingGroupType(null)}
onConfirm={() => {
setGroupType(pendingGroupType);
setPendingGroupType(null);
}}
/>
)}

<footer className="mt-14 pt-4 border-t border-t-text-grey-6">
<div className="flex">
{group && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,136 @@ describe('CreateEditGroupForm', () => {
});
});
});

[
// Do not warn when changing from the default type of "private" to a public
// type, when creating a new group.
{
oldType: null,
newType: 'open',
expectedWarning: null,
annotationCount: 0,
},
{
// Warn when making annotations public (open group)
oldType: 'private',
newType: 'open',
annotationCount: 2,
expectedWarning: {
title: 'Make 2 annotations public?',
message:
'Are you sure you want to make "Test Name" an open group? 2 annotations that are visible only to members of "Test Name" will become publicly visible.',
confirmAction: 'Make annotations public',
},
},
{
// Warn when making annotations public (restricted group)
oldType: 'private',
newType: 'restricted',
annotationCount: 5,
expectedWarning: {
title: 'Make 5 annotations public?',
message:
'Are you sure you want to make "Test Name" a restricted group? 5 annotations that are visible only to members of "Test Name" will become publicly visible.',
confirmAction: 'Make annotations public',
},
},
{
// Warn when making annotations private
oldType: 'open',
newType: 'private',
annotationCount: 3,
expectedWarning: {
title: 'Make 3 annotations private?',
message:
'Are you sure you want to make "Test Name" a private group? 3 annotations that are publicly visible will become visible only to members of "Test Name".',
confirmAction: 'Make annotations private',
},
},
{
// Don't warn if there are no annotations
oldType: 'open',
newType: 'private',
annotationCount: 0,
expectedWarning: null,
},
{
// Don't warn if the old and new types are both public
oldType: 'open',
newType: 'restricted',
annotationCount: 3,
expectedWarning: null,
},
].forEach(({ oldType, newType, annotationCount, expectedWarning }) => {
it('shows warning when changing group type between private and public', () => {
if (oldType !== null) {
config.context.group = {
pubid: 'testid',
name: 'Test Name',
description: 'Test group description',
link: 'https://example.com/groups/testid',
type: oldType,
num_annotations: annotationCount,
};
}

const { wrapper } = createWrapper();
setSelectedGroupType(wrapper, newType);

if (expectedWarning) {
const warning = wrapper.find('WarningDialog');
assert.isTrue(warning.exists());
assert.equal(warning.prop('title'), expectedWarning.title);
assert.equal(warning.prop('message'), expectedWarning.message);
assert.equal(
warning.prop('confirmAction'),
expectedWarning.confirmAction,
);
} else {
assert.isFalse(wrapper.exists('WarningDialog'));
}
});
});

it('updates group type if change is confirmed', async () => {
config.context.group = {
pubid: 'testid',
name: 'Test Name',
description: 'Test group description',
link: 'https://example.com/groups/testid',
type: 'private',
num_annotations: 3,
};

const { wrapper } = createWrapper();
setSelectedGroupType(wrapper, 'open');

const warning = wrapper.find('WarningDialog');
act(() => warning.prop('onConfirm')());
wrapper.update();

assert.equal(getSelectedGroupType(wrapper), 'open');
});

it('does not update group type if change is canceled', async () => {
config.context.group = {
pubid: 'testid',
name: 'Test Name',
description: 'Test group description',
link: 'https://example.com/groups/testid',
type: 'private',
num_annotations: 3,
};

const { wrapper } = createWrapper();
setSelectedGroupType(wrapper, 'open');

const warning = wrapper.find('WarningDialog');
act(() => warning.prop('onCancel')());
wrapper.update();

assert.equal(getSelectedGroupType(wrapper), 'private');
});
});

async function assertInLoadingState(wrapper, inLoadingState) {
Expand Down
1 change: 1 addition & 0 deletions h/static/scripts/group-forms/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ConfigObject = {
description: string;
link: string;
type: GroupType;
num_annotations: number;
} | null;
};
features: {
Expand Down

0 comments on commit dbd6892

Please sign in to comment.