Skip to content

Commit 1cfb11c

Browse files
committed
Pull callbacks and state up into higher-level components
Make `AnnotationPublishControl` more of a controlled component by pulling callbacks and state up to `AnnotationEditor`. Provide a `draft` prop from `Annotation` to `AnnotationEditor`.
1 parent 7b8ff74 commit 1cfb11c

File tree

3 files changed

+95
-81
lines changed

3 files changed

+95
-81
lines changed

src/sidebar/components/Annotation/Annotation.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ function Annotation({
4949

5050
const store = useStoreProxy();
5151

52+
const draft = annotation && store.getDraft(annotation);
53+
5254
const hasQuote = annotation && !!quote(annotation);
5355
const isFocused = annotation && store.isAnnotationFocused(annotation.$tag);
5456
const isSaving = annotation && store.isSavingAnnotation(annotation);
55-
const isEditing = annotation && !!store.getDraft(annotation) && !isSaving;
57+
const isEditing = annotation && !!draft && !isSaving;
5658

5759
const userid = store.profile().userid;
5860
const showActions = !isSaving && !isEditing;
@@ -88,7 +90,9 @@ function Annotation({
8890
<AnnotationBody annotation={annotation} />
8991
)}
9092

91-
{isEditing && <AnnotationEditor annotation={annotation} />}
93+
{isEditing && (
94+
<AnnotationEditor annotation={annotation} draft={draft} />
95+
)}
9296
</>
9397
)}
9498

src/sidebar/components/Annotation/AnnotationEditor.js

Lines changed: 76 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { normalizeKeyName } from '@hypothesis/frontend-shared';
2-
import { useState } from 'preact/hooks';
2+
import { useCallback, useState } from 'preact/hooks';
33

44
import { withServices } from '../../service-context';
5+
import { isReply, isSaved } from '../../helpers/annotation-metadata';
56
import { applyTheme } from '../../helpers/theme';
67
import { useStoreProxy } from '../../store/use-store';
78

@@ -13,12 +14,14 @@ import AnnotationPublishControl from './AnnotationPublishControl';
1314

1415
/**
1516
* @typedef {import("../../../types/api").Annotation} Annotation
17+
* @typedef {import("../../store/modules/drafts").Draft} Draft
1618
* @typedef {import("../../../types/config").SidebarSettings} SidebarSettings
1719
*/
1820

1921
/**
2022
* @typedef AnnotationEditorProps
2123
* @prop {Annotation} annotation - The annotation under edit
24+
* @prop {Draft} draft - The annotation's draft
2225
* @prop {import('../../services/annotations').AnnotationsService} annotationsService
2326
* @prop {SidebarSettings} settings - Injected service
2427
* @prop {import('../../services/toast-messenger').ToastMessengerService} toastMessenger
@@ -32,6 +35,7 @@ import AnnotationPublishControl from './AnnotationPublishControl';
3235
*/
3336
function AnnotationEditor({
3437
annotation,
38+
draft,
3539
annotationsService,
3640
settings,
3741
tags: tagsService,
@@ -43,63 +47,83 @@ function AnnotationEditor({
4347
);
4448

4549
const store = useStoreProxy();
46-
const draft = store.getDraft(annotation);
4750
const group = store.getGroup(annotation.group);
4851

49-
if (!draft) {
50-
// If there's no draft, we can't be in editing mode
51-
return null;
52-
}
53-
5452
const shouldShowLicense =
5553
!draft.isPrivate && group && group.type !== 'private';
5654

5755
const tags = draft.tags;
5856
const text = draft.text;
5957
const isEmpty = !text && !tags.length;
6058

61-
const onEditTags = ({ tags }) => {
62-
store.createDraft(draft.annotation, { ...draft, tags });
63-
};
59+
const onEditTags = useCallback(
60+
({ tags }) => {
61+
store.createDraft(draft.annotation, { ...draft, tags });
62+
},
63+
[draft, store]
64+
);
6465

6566
/**
6667
* Verify `newTag` has content and is not a duplicate; add the tag
6768
*
6869
* @param {string} newTag
6970
* @return {boolean} - `true` if tag is added
7071
*/
71-
const onAddTag = newTag => {
72-
if (!newTag || tags.indexOf(newTag) >= 0) {
73-
// don't add empty or duplicate tags
74-
return false;
75-
}
76-
const tagList = [...tags, newTag];
77-
// Update the tag locally for the suggested-tag list
78-
tagsService.store(tagList);
79-
onEditTags({ tags: tagList });
80-
return true;
81-
};
72+
const onAddTag = useCallback(
73+
newTag => {
74+
if (!newTag || tags.indexOf(newTag) >= 0) {
75+
// don't add empty or duplicate tags
76+
return false;
77+
}
78+
const tagList = [...tags, newTag];
79+
// Update the tag locally for the suggested-tag list
80+
tagsService.store(tagList);
81+
onEditTags({ tags: tagList });
82+
return true;
83+
},
84+
[onEditTags, tags, tagsService]
85+
);
8286

8387
/**
8488
* Remove a tag from the annotation.
8589
*
8690
* @param {string} tag
8791
* @return {boolean} - `true` if tag extant and removed
8892
*/
89-
const onRemoveTag = tag => {
90-
const newTagList = [...tags]; // make a copy
91-
const index = newTagList.indexOf(tag);
92-
if (index >= 0) {
93-
newTagList.splice(index, 1);
94-
onEditTags({ tags: newTagList });
95-
return true;
96-
}
97-
return false;
98-
};
93+
const onRemoveTag = useCallback(
94+
tag => {
95+
const newTagList = [...tags]; // make a copy
96+
const index = newTagList.indexOf(tag);
97+
if (index >= 0) {
98+
newTagList.splice(index, 1);
99+
onEditTags({ tags: newTagList });
100+
return true;
101+
}
102+
return false;
103+
},
104+
[onEditTags, tags]
105+
);
99106

100-
const onEditText = ({ text }) => {
101-
store.createDraft(draft.annotation, { ...draft, text });
102-
};
107+
const onEditText = useCallback(
108+
({ text }) => {
109+
store.createDraft(draft.annotation, { ...draft, text });
110+
},
111+
[draft, store]
112+
);
113+
114+
/**
115+
* @param {boolean} isPrivate
116+
*/
117+
const onSetPrivacy = useCallback(
118+
isPrivate => {
119+
store.createDraft(annotation, { ...draft, isPrivate });
120+
// Persist this as privacy default for future annotations unless this is a reply
121+
if (!isReply(annotation)) {
122+
store.setDefault('annotationPrivacy', isPrivate ? 'private' : 'shared');
123+
}
124+
},
125+
[annotation, draft, store]
126+
);
103127

104128
const onSave = async () => {
105129
// If there is any content in the tag editor input field that has
@@ -115,6 +139,14 @@ function AnnotationEditor({
115139
}
116140
};
117141

142+
// Revert changes to this annotation
143+
const onCancel = useCallback(() => {
144+
store.removeDraft(annotation);
145+
if (!isSaved(annotation)) {
146+
store.removeAnnotations([annotation]);
147+
}
148+
}, [annotation, store]);
149+
118150
// Allow saving of annotation by pressing CMD/CTRL-Enter
119151
/** @param {KeyboardEvent} event */
120152
const onKeyDown = event => {
@@ -150,11 +182,16 @@ function AnnotationEditor({
150182
tagList={tags}
151183
/>
152184
<div className="hyp-u-layout-row annotation__form-actions">
153-
<AnnotationPublishControl
154-
annotation={annotation}
155-
isDisabled={isEmpty}
156-
onSave={onSave}
157-
/>
185+
{group && (
186+
<AnnotationPublishControl
187+
group={group}
188+
isDisabled={isEmpty}
189+
isPrivate={draft.isPrivate}
190+
onCancel={onCancel}
191+
onSave={onSave}
192+
onSetPrivacy={onSetPrivacy}
193+
/>
194+
)}
158195
</div>
159196
{shouldShowLicense && <AnnotationLicense />}
160197
</div>

src/sidebar/components/Annotation/AnnotationPublishControl.js

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
import { Icon, LabeledButton } from '@hypothesis/frontend-shared';
22
import classnames from 'classnames';
33

4-
import { useStoreProxy } from '../../store/use-store';
5-
import { isNew, isReply } from '../../helpers/annotation-metadata';
6-
import { isShared } from '../../helpers/permissions';
74
import { withServices } from '../../service-context';
85
import { applyTheme } from '../../helpers/theme';
96

107
import Menu from '../Menu';
118
import MenuItem from '../MenuItem';
129

1310
/**
14-
* @typedef {import('../../../types/api').Annotation} Annotation
11+
* @typedef {import('../../../types/api').Group} Group
1512
* @typedef {import('../../../types/config').SidebarSettings} SidebarSettings
1613
*/
1714

1815
/**
1916
* @typedef AnnotationPublishControlProps
20-
* @prop {Annotation} annotation
17+
* @prop {Group} group - The group this annotation or draft would publish to
2118
* @prop {boolean} [isDisabled]
2219
* - Should the save button be disabled? Hint: it will be if the annotation has no content
23-
* @prop {() => any} onSave - Callback for save button click
20+
* @prop {boolean} isPrivate - Annotation or draft is "Only Me"
21+
* @prop {() => void} onCancel - Callback for cancel button click
22+
* @prop {() => void} onSave - Callback for save button click
23+
* @prop {(isPrivate: boolean) => void} onSetPrivacy - Callback for save button click
2424
* @prop {SidebarSettings} settings - Injected service
2525
*/
2626

@@ -32,41 +32,14 @@ import MenuItem from '../MenuItem';
3232
* @param {AnnotationPublishControlProps} props
3333
*/
3434
function AnnotationPublishControl({
35-
annotation,
35+
group,
3636
isDisabled,
37+
isPrivate,
38+
onCancel,
3739
onSave,
40+
onSetPrivacy,
3841
settings,
3942
}) {
40-
const store = useStoreProxy();
41-
const draft = store.getDraft(annotation);
42-
const group = store.getGroup(annotation.group);
43-
44-
if (!group) {
45-
// If there is no group, then don't render anything as a missing group
46-
// may mean the group is not loaded yet.
47-
return null;
48-
}
49-
50-
const isPrivate = draft ? draft.isPrivate : !isShared(annotation.permissions);
51-
52-
const publishDestination = isPrivate ? 'Only Me' : group.name;
53-
54-
// Revert changes to this annotation
55-
const onCancel = () => {
56-
store.removeDraft(annotation);
57-
if (isNew(annotation)) {
58-
store.removeAnnotations([annotation]);
59-
}
60-
};
61-
62-
const onSetPrivacy = level => {
63-
store.createDraft(annotation, { ...draft, isPrivate: level === 'private' });
64-
// Persist this as privacy default for future annotations unless this is a reply
65-
if (!isReply(annotation)) {
66-
store.setDefault('annotationPrivacy', level);
67-
}
68-
};
69-
7043
const buttonStyle = applyTheme(
7144
['ctaTextColor', 'ctaBackgroundColor'],
7245
settings
@@ -93,7 +66,7 @@ function AnnotationPublishControl({
9366
size="large"
9467
variant="primary"
9568
>
96-
Post to {publishDestination}
69+
Post to {isPrivate ? 'Only Me' : group.name}
9770
</LabeledButton>
9871
{/* This wrapper div is necessary because of peculiarities with
9972
Safari: see https://github.com/hypothesis/client/issues/2302 */}
@@ -125,13 +98,13 @@ function AnnotationPublishControl({
12598
icon={group.type === 'open' ? 'public' : 'groups'}
12699
label={group.name}
127100
isSelected={!isPrivate}
128-
onClick={() => onSetPrivacy('shared')}
101+
onClick={() => onSetPrivacy(false)}
129102
/>
130103
<MenuItem
131104
icon="lock"
132105
label="Only Me"
133106
isSelected={isPrivate}
134-
onClick={() => onSetPrivacy('private')}
107+
onClick={() => onSetPrivacy(true)}
135108
/>
136109
</Menu>
137110
</div>

0 commit comments

Comments
 (0)