Skip to content

Commit 34724ee

Browse files
committed
Document fixes and Note component #741
1 parent ae3abb8 commit 34724ee

File tree

15 files changed

+348
-66
lines changed

15 files changed

+348
-66
lines changed

browser/data-browser/src/chunks/RTE/BubbleMenu.tsx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,28 @@ export function BubbleMenu({
3434
const editor = useTipTapEditor();
3535
const [linkMenuOpen, setLinkMenuOpen] = useState(false);
3636

37-
const { isBold, isItalic, isStrikethrough, isBlockquote, isCode, isLink } =
38-
useEditorState({
39-
editor,
40-
selector: snapshot => ({
41-
isBold: snapshot.editor.isActive('bold'),
42-
isItalic: snapshot.editor.isActive('italic'),
43-
isStrikethrough: snapshot.editor.isActive('strike'),
44-
isBlockquote: snapshot.editor.isActive('blockquote'),
45-
isCode: snapshot.editor.isActive('code'),
46-
isLink: snapshot.editor.isActive('link'),
47-
}),
48-
});
37+
const {
38+
isBold,
39+
isItalic,
40+
isStrikethrough,
41+
isBlockquote,
42+
isCode,
43+
isLink,
44+
isInitialized,
45+
} = useEditorState({
46+
editor,
47+
selector: snapshot => ({
48+
isBold: snapshot.editor.isActive('bold'),
49+
isItalic: snapshot.editor.isActive('italic'),
50+
isStrikethrough: snapshot.editor.isActive('strike'),
51+
isBlockquote: snapshot.editor.isActive('blockquote'),
52+
isCode: snapshot.editor.isActive('code'),
53+
isLink: snapshot.editor.isActive('link'),
54+
isInitialized: snapshot.editor.isInitialized,
55+
}),
56+
});
4957

50-
if (!editor.isInitialized) {
58+
if (!isInitialized) {
5159
return <></>;
5260
}
5361

browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,17 @@ import {
4646
ResourceNodeInline,
4747
} from './ResourceExtension/ResourceNode';
4848
import { IsInRTEContex } from '@hooks/useIsInRTE';
49-
import { FaGripVertical, FaLink, FaTable } from 'react-icons/fa6';
49+
import { FaCircleInfo, FaGripVertical, FaLink, FaTable } from 'react-icons/fa6';
5050
import { useUpload } from '@hooks/useUpload';
5151
import FileHandler from '@tiptap/extension-file-handler';
5252
import { supportedImageTypes } from '@views/File/fileTypeUtils';
5353
import type { SuggestionItem } from './types';
5454
import { useNewResourceUI } from '@components/forms/NewForm/useNewResourceUI';
5555
import { addIf } from '@helpers/addIf';
56+
import toast from 'react-hot-toast';
57+
import { Row } from '@components/Row';
58+
import { Button } from '@components/Button';
59+
import { Note } from './NoteExtention/NoteExtention';
5660

5761
export type CollaborativeEditorProps = {
5862
placeholder?: string;
@@ -107,6 +111,7 @@ export default function CollaborativeEditor({
107111
undoRedo: false,
108112
link: false,
109113
}),
114+
Note,
110115
Typography,
111116
Link.extend({
112117
parseHTML: () => [
@@ -155,6 +160,19 @@ export default function CollaborativeEditor({
155160
}),
156161
SlashCommands.configure({
157162
suggestion: buildSuggestion(document.body, [
163+
{
164+
title: 'Note',
165+
id: 'note',
166+
icon: FaCircleInfo,
167+
command: ({ range, editor: internalEditor }) => {
168+
internalEditor
169+
.chain()
170+
.focus()
171+
.deleteRange(range)
172+
.toggleNote()
173+
.run();
174+
},
175+
},
158176
{
159177
title: 'Resource',
160178
id: 'resource',
@@ -244,6 +262,7 @@ export default function CollaborativeEditor({
244262
}),
245263
],
246264
editable: canWrite,
265+
enableContentCheck: true,
247266
onBlur,
248267
editorProps: {
249268
attributes: {
@@ -254,6 +273,30 @@ export default function CollaborativeEditor({
254273
spellcheck: 'true',
255274
},
256275
},
276+
onContentError({ editor: currentEditor, error, disableCollaboration }) {
277+
// Removes the collaboration extension.
278+
disableCollaboration();
279+
280+
// Since the content is invalid, we don't want to emit an update
281+
// Preventing synchronization with other editors or to a server
282+
const emitUpdate = false;
283+
284+
// Disable the editor to prevent further user input
285+
currentEditor.setEditable(false, emitUpdate);
286+
287+
console.error(error);
288+
// Maybe show a notification to the user that they need to refresh the app
289+
toast.error(
290+
<Row wrapItems>
291+
There was an error in the editor, please refresh the page to
292+
continue.{' '}
293+
<Button subtle onClick={() => window.location.reload()}>
294+
Refresh
295+
</Button>
296+
</Row>,
297+
{ duration: Infinity },
298+
);
299+
},
257300
},
258301
[canWrite, drive],
259302
);

browser/data-browser/src/chunks/RTE/ColorMenu.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,16 @@ export const ColorMenu: React.FC = () => {
3434
editor,
3535
selector: snapshot => {
3636
return {
37-
selectedTextColor: snapshot.editor.getAttributes('textStyle').color,
37+
selectedTextColor:
38+
(snapshot.editor.getAttributes('textStyle').color as
39+
| string
40+
| undefined
41+
| null) || undefined,
3842
selectedBackgroundColor:
39-
snapshot.editor.getAttributes('textStyle').backgroundColor,
43+
(snapshot.editor.getAttributes('textStyle').backgroundColor as
44+
| string
45+
| undefined
46+
| null) || undefined,
4047
};
4148
},
4249
});
@@ -51,7 +58,13 @@ export const ColorMenu: React.FC = () => {
5158
defaultBackgroundColors,
5259
);
5360

54-
const setTextColor = (color: string) => {
61+
const setTextColor = (color: string | undefined) => {
62+
if (color === undefined) {
63+
editor.chain().focus().unsetColor().run();
64+
65+
return;
66+
}
67+
5568
editor.chain().setColor(color).run();
5669
setLastUsedTextColors(prev => [
5770
color,
@@ -61,7 +74,13 @@ export const ColorMenu: React.FC = () => {
6174
]);
6275
};
6376

64-
const setBackgroundColor = (color: string) => {
77+
const setBackgroundColor = (color: string | undefined) => {
78+
if (color === undefined) {
79+
editor.chain().focus().unsetBackgroundColor().run();
80+
81+
return;
82+
}
83+
6584
editor.chain().setBackgroundColor(color).run();
6685
setLastUsedBackgroundColor(prev => [
6786
color,
@@ -144,7 +163,10 @@ export const ColorMenu: React.FC = () => {
144163
);
145164
};
146165

147-
const useColor = (initialColor: string, onSelect: (color: string) => void) => {
166+
const useColor = (
167+
initialColor: string | undefined,
168+
onSelect: (color: string | undefined) => void,
169+
) => {
148170
const [isChanging, setIsChanging] = useState(false);
149171
const colorRef = useRef(initialColor);
150172

@@ -203,14 +225,14 @@ const ColorButton = styled.button<{ color: string }>`
203225

204226
interface ColorInputProps {
205227
label: string;
206-
value: string;
228+
value: string | undefined;
207229
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
208230
onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
209231
}
210232

211233
const ColorInput: React.FC<ColorInputProps> = ({
212234
label,
213-
value,
235+
value = '#ffffff',
214236
onChange,
215237
onBlur,
216238
}) => {
@@ -221,7 +243,7 @@ const ColorInput: React.FC<ColorInputProps> = ({
221243
</div>
222244
<HiddenColorInput
223245
type='color'
224-
value={value ?? '#ffffff'}
246+
value={value}
225247
onChange={onChange}
226248
onBlur={onBlur}
227249
/>
@@ -239,8 +261,8 @@ const HiddenColorInput = styled.input`
239261
width: 1px;
240262
`;
241263

242-
const ColorInputLabel = styled.label<{ color: string }>`
243-
--CIL_foreground: ${p => readableColor(p.color ?? p.theme.colors.bg)};
264+
const ColorInputLabel = styled.label<{ color: string | undefined }>`
265+
--CIL_foreground: ${p => readableColor(p.color || p.theme.colors.bg)};
244266
cursor: pointer;
245267
position: relative;
246268
gap: 0.5rem;

browser/data-browser/src/chunks/RTE/ImagePicker.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const MarkdownEditorImage = ({
5151
const [urlValue, setUrlValue] = useState<string>();
5252
const [selectedSubject, setSelectedSubject] = useState<string>();
5353
const [altText, setAltText] = useState<string>();
54-
54+
const [imageError, setImageError] = useState<boolean>(false);
5555
const [urlValid, urlRef] = useHTMLFormFieldValidation();
5656

5757
const canSave = () => {
@@ -80,6 +80,14 @@ const MarkdownEditorImage = ({
8080
}
8181
: undefined;
8282

83+
if (imageError) {
84+
return (
85+
<NodeViewWrapper>
86+
<ImageError selected={selected}>Failed to load image.</ImageError>
87+
</NodeViewWrapper>
88+
);
89+
}
90+
8391
if (node.attrs.src) {
8492
return (
8593
<NodeViewWrapper>
@@ -88,6 +96,7 @@ const MarkdownEditorImage = ({
8896
src={node.attrs.src}
8997
alt=''
9098
selected={selected}
99+
onError={() => setImageError(true)}
91100
/>
92101
</NodeViewWrapper>
93102
);
@@ -202,3 +211,23 @@ const StyledInputWrapper = styled(InputWrapper)`
202211
border-color: ${p => p.theme.colors.alert} !important;
203212
}
204213
`;
214+
215+
const ImageError = styled.div<SelectableProps>`
216+
background-color: ${p => p.theme.colors.bg1};
217+
padding: ${p => p.theme.size()};
218+
border-radius: ${p => p.theme.radius};
219+
color: ${p => p.theme.colors.textLight};
220+
width: 50%;
221+
aspect-ratio: 1/1;
222+
display: grid;
223+
place-items: center;
224+
font-size: 1.5rem;
225+
font-weight: 500;
226+
${transition('box-shadow', 'filter')}
227+
228+
.tiptap:focus-within & {
229+
box-shadow: 0 0 0 2px
230+
${p => (p.selected ? p.theme.colors.main : 'transparent')};
231+
filter: ${p => (p.selected ? 'brightness(0.9)' : 'none')};
232+
}
233+
`;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Row } from '@components/Row';
2+
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react';
3+
import { FaCircleInfo } from 'react-icons/fa6';
4+
import { styled } from 'styled-components';
5+
6+
export const NoteComponent = () => {
7+
return (
8+
<StyledNodeViewWrapper>
9+
<Title center contentEditable={false} gap='1ch'>
10+
<FaCircleInfo />
11+
Note
12+
</Title>
13+
<NodeViewContent />
14+
</StyledNodeViewWrapper>
15+
);
16+
};
17+
18+
const StyledNodeViewWrapper = styled(NodeViewWrapper)`
19+
background-color: ${p => p.theme.colors.mainSelectedBg};
20+
padding: 1rem;
21+
border-left: 3px solid ${p => p.theme.colors.main};
22+
width: 100%;
23+
margin-bottom: ${p => p.theme.size()};
24+
& p:last-child {
25+
margin-bottom: 0;
26+
}
27+
`;
28+
29+
const Title = styled(Row)`
30+
font-weight: 600;
31+
font-size: 1.1rem;
32+
color: ${p => p.theme.colors.mainSelectedFg};
33+
margin-bottom: ${p => p.theme.size(2)};
34+
`;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { mergeAttributes, Node } from '@tiptap/core';
2+
import { ReactNodeViewRenderer } from '@tiptap/react';
3+
import { NoteComponent } from './NoteComponent';
4+
5+
declare module '@tiptap/core' {
6+
interface Commands<ReturnType> {
7+
note: {
8+
toggleNote: () => ReturnType;
9+
};
10+
}
11+
}
12+
13+
export const Note = Node.create({
14+
name: 'note-block',
15+
group: 'block',
16+
content: 'block*',
17+
defining: true,
18+
renderHTML({ HTMLAttributes }) {
19+
return ['note-block', mergeAttributes(HTMLAttributes), 0];
20+
},
21+
22+
parseHTML() {
23+
return [
24+
{
25+
tag: 'note-block',
26+
},
27+
];
28+
},
29+
30+
addNodeView() {
31+
return ReactNodeViewRenderer(NoteComponent);
32+
},
33+
34+
addCommands() {
35+
return {
36+
toggleNote:
37+
() =>
38+
({ commands }) => {
39+
return commands.wrapIn(this.type.name);
40+
},
41+
};
42+
},
43+
});

browser/data-browser/src/chunks/RTE/ResourceExtension/ResourceComponent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export const ResourceComponent = (
2424
return (
2525
<RTENodeViewWrapper wide={wide} key={props.node.attrs.subject}>
2626
<ErrorBoundary>
27-
<Component subject={props.node.attrs.subject} />
27+
<Component
28+
subject={props.node.attrs.subject}
29+
highlight={props.selected}
30+
/>
2831
</ErrorBoundary>
2932
</RTENodeViewWrapper>
3033
);

0 commit comments

Comments
 (0)