Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit fa58397

Browse files
authored
feat: DEV-2887: Comments for annotation editor ui (#774)
* wip: DEV-2887: comment form * feat: DEV-2887: comment form styling and auto-resize behaviour * feat: DEV-2887: comment allow form to conditionally be controlled with proxied events * chore: DEV-2887: refactor TextArea to its own component * fix: DEV-2887: padding for inline action textarea * feat: DEV-2887: CommentsList and styles * wip: DEV-2887: comment submit form reset * wip: DEV-2887: comment section style updates * feat: DEV-2887: CommentStore api event proxy * feat: DEV-2887: move Comment up to top level of stores * feat: DEV-2887: wire up CommentList and CommentForm with api proxy * feat: DEV-2887: Ensure the replaceId works temporarily with the generatedId until the api supports returning on create * feat: DEV-2887: generated comment ids need to be unable to collide with real ones * fix: DEV-2887: ensure annotation id ref is not stale * fix: DEV-2887: ensure comments list updates are protected from unmounting component * feat: DEV-2887: CommentItem action styling * fix: DEV-2887: remove mocked user data * fix: DEV-2887: ensure comments is available to non outliner sidebar * fix: DEV-2887: ensure comments in non outliner sidebar are styled accordingly * fix: DEV-2887: ensure currentUser is pointing at the correct user ref * fix: DEV-2887: ensure current user is set on app store * fix: DEV-2887: auto size menu to contents * fix: DEV-2887: allow TextArea to submit on CTRL or CMD * fix: DEV-2887: allow TextArea to submit on CTRL or CMD * feat: DEV-2887: persist queued comments on annotation creation * fix: DEV-3002: Predictions should not have comments * fix: DEV-2991: Cancelling skip retains comments * fix: DEV-2986: Warn about comments being lost if the user has unpersisted comments * fix: DEV-2991: make comment persist sync to guarantee order * fix: DEV-2986: change commentStore method to hasUnsaved * fix: DEV-2986: non outliner view comments cancel skip should retain comments
1 parent 8923563 commit fa58397

File tree

27 files changed

+966
-17
lines changed

27 files changed

+966
-17
lines changed

src/assets/icons/ellipsis.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/icons/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export { ReactComponent as LsDate } from "./date.svg";
1919
export { ReactComponent as IconPlusCircle } from "./plus_circle.svg";
2020
export { ReactComponent as IconSlow } from "./slow.svg";
2121
export { ReactComponent as IconFast } from "./fast.svg";
22+
export { ReactComponent as IconEllipsis } from "./ellipsis.svg";
2223

2324
export { ReactComponent as IconCheck } from "./check.svg";
2425
export { ReactComponent as IconCross } from "./cross.svg";

src/assets/icons/send.svg

Lines changed: 3 additions & 0 deletions
Loading

src/common/Menu/Menu.styl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@
6666
height 32px
6767
font-size 16px
6868

69+
&_size_auto &__item
70+
padding 0 16px
71+
72+
&_size_auto
73+
width auto !important
74+
min-width initial !important
75+
76+
6977
&_size_small &__item
7078
height 24px
7179
font-size 14px

src/common/TextArea/TextArea.styl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.textarea
2+
--textarea-surface-color #FAFAFA
3+
--textarea-border-size 1px
4+
--textarea-border-color #DFDFDF
5+
--textarea-border-radius 4px
6+
--textarea-font-size 16px
7+
--textarea-line-height 24px
8+
--textarea-min-height 40px
9+
--textarea-letter-spacing 0.5px
10+
--textarea-padding 8px 16px
11+
12+
// @todo refactor the overall input styles to not have to use important to override
13+
&_inline
14+
--textarea-padding 8px var(--textarea-min-height) 8px 16px
15+
16+
display flex
17+
align-items center
18+
min-height var(--textarea-min-height) !important
19+
border var(--textarea-border-size) solid var(--textarea-border-color) !important
20+
border-radius var(--textarea-border-radius) !important
21+
background-color var(--textarea-surface-color) !important
22+
padding var(--textarea-padding) !important
23+
font-size var(--textarea-font-size) !important
24+
line-height var(--textarea-line-height) !important
25+
letter-spacing var(--textarea-letter-spacing) !important
26+
27+
grid-row 1 / 2
28+
grid-column 1 / span 2
29+
padding-right var(--textarea-min-height)
30+
scrollbar-width none
31+
-ms-overflow-style none
32+
33+
&::-webkit-scrollbar
34+
width 0
35+
height 0
36+
37+
&_autosize
38+
resize none !important

src/common/TextArea/TextArea.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { FC, MutableRefObject, useCallback, useEffect, useRef } from "react";
2+
import { debounce } from "lodash";
3+
import { cn } from "../../utils/bem";
4+
import { isMacOS } from "../../utils/utilities";
5+
6+
import "./TextArea.styl";
7+
import mergeRefs from "../Utils/mergeRefs";
8+
9+
export type TextAreaProps = {
10+
value?: string|null,
11+
onSubmit?: () => void|Promise<void>,
12+
onChange?: (value: string) => void,
13+
onInput?: (value: string) => void,
14+
ref?: MutableRefObject<HTMLTextAreaElement>,
15+
actionRef?: MutableRefObject<{ update?: (text?: string) => void }>,
16+
rows?: number,
17+
maxRows?: number,
18+
autoSize?: boolean,
19+
className?: string,
20+
placeholder?: string,
21+
name?: string,
22+
id?: string,
23+
}
24+
25+
export const TextArea: FC<TextAreaProps> = ({
26+
ref,
27+
actionRef,
28+
onChange: _onChange,
29+
onInput: _onInput,
30+
onSubmit,
31+
value,
32+
autoSize = true,
33+
rows = 1,
34+
maxRows = 4,
35+
className,
36+
...props
37+
}) => {
38+
39+
const inlineAction = !!onSubmit;
40+
41+
const rootClass = cn('textarea');
42+
const classList = [
43+
rootClass.mod({ inline: inlineAction, autosize: autoSize }),
44+
className,
45+
].join(" ").trim();
46+
47+
const autoGrowRef = useRef({
48+
rows,
49+
maxRows: Math.max(maxRows - 1, 1),
50+
lineHeight: 24,
51+
maxHeight: Infinity,
52+
});
53+
const textAreaRef = useRef<HTMLTextAreaElement>(null);
54+
55+
const resizeTextArea = useCallback(debounce(() => {
56+
const textarea = textAreaRef.current;
57+
58+
if (!textarea || !autoGrowRef.current || !textAreaRef.current) return;
59+
60+
if (autoGrowRef.current.maxHeight === Infinity) {
61+
textarea.style.height = 'auto';
62+
const currentValue = textAreaRef.current.value;
63+
64+
textAreaRef.current.value = "";
65+
autoGrowRef.current.lineHeight = (textAreaRef.current.scrollHeight / autoGrowRef.current.rows);
66+
autoGrowRef.current.maxHeight = (autoGrowRef.current.lineHeight * autoGrowRef.current.maxRows);
67+
68+
textAreaRef.current.value = currentValue;
69+
}
70+
71+
let newHeight: number;
72+
73+
if(textarea.scrollHeight > autoGrowRef.current.maxHeight){
74+
textarea.style.overflowY = 'scroll';
75+
newHeight = autoGrowRef.current.maxHeight;
76+
} else {
77+
textarea.style.overflowY = 'hidden';
78+
textarea.style.height = 'auto';
79+
newHeight = textarea.scrollHeight;
80+
}
81+
const contentLength = textarea.value.length;
82+
const cursorPosition = textarea.selectionStart;
83+
84+
requestAnimationFrame(() => {
85+
textarea.style.height = `${newHeight}px`;
86+
87+
if (contentLength === cursorPosition) {
88+
textarea.scrollTop = textarea.scrollHeight;
89+
}
90+
});
91+
}, 10, { leading: true }), []);
92+
93+
if (actionRef) {
94+
actionRef.current = {
95+
update: (text = "") => {
96+
if (!textAreaRef.current) return;
97+
98+
textAreaRef.current.value = text;
99+
resizeTextArea();
100+
},
101+
};
102+
}
103+
104+
const onInput = useCallback((e: any) => {
105+
_onInput?.(e.target.value);
106+
resizeTextArea();
107+
}, [_onInput]);
108+
109+
const onChange = useCallback((e: any) => {
110+
_onChange?.(e.target.value);
111+
resizeTextArea();
112+
}, [_onChange]);
113+
114+
useEffect(() => {
115+
const resize = new ResizeObserver(resizeTextArea);
116+
117+
resize.observe(textAreaRef.current as any);
118+
119+
return () => {
120+
if (textAreaRef.current) {
121+
resize.unobserve(textAreaRef.current as any);
122+
}
123+
};
124+
}, []);
125+
126+
useEffect(() => {
127+
if (textAreaRef.current) {
128+
textAreaRef.current.value = value || "";
129+
resizeTextArea();
130+
}
131+
}, [value]);
132+
133+
useEffect(() => {
134+
if (!onSubmit) return;
135+
136+
const listener = (event: KeyboardEvent) => {
137+
if (!textAreaRef.current) return;
138+
if (event.key === "Enter" && (event.ctrlKey || isMacOS() && event.metaKey)) {
139+
onSubmit();
140+
}
141+
};
142+
143+
if (textAreaRef.current) {
144+
textAreaRef.current.addEventListener("keydown", listener);
145+
}
146+
return () => {
147+
if (textAreaRef.current) {
148+
textAreaRef.current.removeEventListener("keydown", listener);
149+
}
150+
};
151+
}, [onSubmit]);
152+
153+
154+
return (
155+
<textarea ref={mergeRefs(textAreaRef, ref)} className={classList} rows={autoGrowRef.current.rows} onChange={onChange} onInput={onInput} {...props}></textarea>
156+
);
157+
};

src/common/Utils/mergeRefs.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { MutableRefObject } from "react";
2+
3+
export default function mergeRefs(...inputRefs: (MutableRefObject<any>|undefined|null)[]) {
4+
const filteredInputRefs = inputRefs.filter(Boolean) as MutableRefObject<any>[];
5+
6+
if (filteredInputRefs.length <= 1) {
7+
return filteredInputRefs[0];
8+
}
9+
10+
return (ref: any) => {
11+
filteredInputRefs.forEach((inputRef: MutableRefObject<any>|((ref: MutableRefObject<any>) => void)) => {
12+
if (typeof inputRef === 'function') {
13+
inputRef(ref);
14+
} else {
15+
inputRef.current = ref;
16+
}
17+
});
18+
};
19+
}

src/common/Utils/useMounted.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useEffect, useRef } from "react";
2+
3+
/**
4+
* Protects async tasks from causing memory leaks in other effects/callbacks.
5+
* Wrap any set states within a component with
6+
*
7+
* if (mounted.current) { ... }
8+
*/
9+
export const useMounted = () => {
10+
const mounted = useRef(true);
11+
12+
useEffect(() => {
13+
mounted.current = true;
14+
return () => {
15+
mounted.current = false;
16+
};
17+
}, []);
18+
19+
return mounted;
20+
};
21+

src/components/AnnotationTab/AnnotationTab.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { observer } from "mobx-react";
2+
import { Block, Elem } from "../../utils/bem";
23
import { CurrentEntity } from "../CurrentEntity/CurrentEntity";
34
import Entities from "../Entities/Entities";
45
import Entity from "../Entity/Entity";
56
import Relations from "../Relations/Relations";
7+
import { Comments } from "../Comments/Comments";
8+
9+
import './CommentsSection.styl';
610

711
export const AnnotationTab = observer(({ store }) => {
812
const as = store.annotationStore;
@@ -41,6 +45,21 @@ export const AnnotationTab = observer(({ store }) => {
4145
{hasSegmentation && (
4246
<Relations store={store} item={annotation} />
4347
)}
48+
49+
{store.hasInterface("annotations:comments") && annotation.commentStore.isCommentable && (
50+
<Block name="comments-section">
51+
<Elem name="header">
52+
<Elem name="title">Comments</Elem>
53+
</Elem>
54+
55+
<Elem name="content">
56+
<Comments
57+
commentStore={annotation.commentStore}
58+
cacheKey={`task.${store.task.id}`}
59+
/>
60+
</Elem>
61+
</Block>
62+
)}
4463
</>
4564
);
4665
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.comments-section
2+
border-top 1px solid rgba(0,0,0,0.1)
3+
4+
&__header
5+
display flex
6+
height 46px
7+
justify-content space-between
8+
padding 12px 15px
9+
align-items center
10+
font-weight 500
11+
font-size 16px
12+
line-height 22px
13+
14+
&__title
15+
flex 1

0 commit comments

Comments
 (0)