Skip to content

Commit 1aabf1f

Browse files
authored
Merge pull request #133 from atomly/feature/editor-improvements-and-error-handling
feat: enhance editor with error handling and UI improvements
2 parents 5efc7d2 + 30cb281 commit 1aabf1f

File tree

10 files changed

+203
-46
lines changed

10 files changed

+203
-46
lines changed

.github/renovate.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,55 @@
11
{
22
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
33
"automerge": true,
4+
"automergeType": "pr",
5+
"branchConcurrentLimit": 4,
46
"extends": ["config:base"],
7+
8+
"lockFileMaintenance": {
9+
"automerge": true,
10+
"enabled": true,
11+
"schedule": ["after 9pm on Sunday"]
12+
},
13+
514
"npm": {
615
"fileMatch": ["(^|/)package\\.json$", "(^|/)package\\.json\\.hbs$"]
716
},
17+
818
"packageRules": [
919
{
1020
"enabled": false,
1121
"matchPackagePatterns": ["^@acme/"]
22+
},
23+
{
24+
"automerge": true,
25+
"groupName": "all non-major dependencies",
26+
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
27+
"stabilityDays": 7
28+
},
29+
{
30+
"automerge": true,
31+
"groupName": "dev tooling (weekly batch)",
32+
"matchDepTypes": ["devDependencies"],
33+
"stabilityDays": 7
34+
},
35+
{
36+
"automerge": false,
37+
"groupName": "major upgrades (manual)",
38+
"matchUpdateTypes": ["major"]
39+
},
40+
{
41+
"groupName": "TS/ESLint toolchain",
42+
"matchPackagePatterns": ["^typescript$", "^eslint", "^@types/"],
43+
"stabilityDays": 3
1244
}
1345
],
46+
"prConcurrentLimit": 4,
47+
48+
"prHourlyLimit": 2,
1449
"rangeStrategy": "bump",
50+
"rebaseWhen": "never",
51+
52+
"schedule": ["after 9pm on Sunday"],
53+
"timezone": "America/New_York",
1554
"updateInternalDeps": true
1655
}

apps/web/src/app/(app)/@chatSidebar/_components/chat-sidebar-trigger.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ export default function ChatSidebarTrigger() {
2121
data-slot="sidebar-trigger"
2222
onClick={toggleSidebar}
2323
size="icon"
24-
variant="ghost"
25-
className="fixed right-2 bottom-2 hidden size-12 cursor-pointer rounded-full border bg-sidebar md:flex"
24+
className="fixed right-2 bottom-2 hidden size-10 cursor-pointer rounded-full border md:flex"
2625
>
2726
<Brain className="size-6" />
2827
<span className="sr-only">Toggle Chat Sidebar</span>

apps/web/src/app/(app)/journal/_components/journal-entry-editor.tsx

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import {
1010
useEffect,
1111
useMemo,
1212
useRef,
13+
useState,
1314
} from "react";
1415
import { useDebouncedCallback } from "use-debounce";
1516
import { useJournlAgentAwareness } from "~/ai/agents/use-journl-agent-awareness";
1617
import { BlockEditor } from "~/components/editor/block-editor";
18+
import { BlockEditorErrorOverlay } from "~/components/editor/block-editor-error-overlay";
1719
import {
1820
BlockEditorFormattingToolbar,
1921
BlockEditorSlashMenu,
@@ -153,30 +155,46 @@ export function JournalEntryContent({
153155

154156
type JournalEntryEditorProps = {
155157
debounceTime?: number;
156-
onCreate?: (newEntry: TimelineEntry) => void;
158+
onCreateAction?: (newEntry: TimelineEntry) => void;
157159
};
158160

159161
export function JournalEntryEditor({
160162
debounceTime = DEFAULT_DEBOUNCE_TIME,
161-
onCreate,
163+
onCreateAction,
162164
}: JournalEntryEditorProps) {
163165
const trpc = useTRPC();
164166
const pendingChangesRef = useRef<BlockTransaction[]>([]);
165167
const { initialBlocks, documentId, date } = useJournalEntry();
166168
const { rememberEditor, forgetEditor } = useJournlAgentAwareness();
167169
const editor = useBlockEditor({ initialBlocks });
170+
const [isOverlayOpen, setOverlayOpen] = useState(false);
171+
172+
/**
173+
* Handles the error state of the editor.
174+
*
175+
* @privateRemarks
176+
*
177+
* Using replace() to avoid creating a new history entry
178+
*/
179+
function handleError() {
180+
setOverlayOpen(true);
181+
requestAnimationFrame(() => {
182+
location.replace(location.href);
183+
});
184+
}
168185

169186
const { mutate, isPending } = useMutation({
170187
...trpc.journal.saveTransactions.mutationOptions({}),
171-
// ! TODO: When the mutation fails we need to revert the changes to the editor just like Notion does.
172-
// ! To do this we can use `onError` and `editor.undo()`, without calling the transactions. We might have to get creative.
173-
// ! Maybe we can refetch the blocks after an error instead of `undo`?
188+
onError: (error) => {
189+
console.error("[JournalEntryEditor] error 👀", error);
190+
handleError();
191+
},
174192
onSuccess: (data) => {
175193
if (pendingChangesRef.current.length > 0) {
176194
debouncedMutate();
177195
}
178196
if (!documentId && data) {
179-
onCreate?.(data);
197+
onCreateAction?.(data);
180198
}
181199
},
182200
});
@@ -202,18 +220,21 @@ export function JournalEntryEditor({
202220
}, [date, editor, rememberEditor, forgetEditor]);
203221

204222
return (
205-
<BlockEditor
206-
editor={editor}
207-
initialBlocks={initialBlocks}
208-
onChange={handleEditorChange}
209-
// Disabling the default because we're using a formatting toolbar with the AI option.
210-
formattingToolbar={false}
211-
// Disabling the default because we're using a slash menu with the AI option.
212-
slashMenu={false}
213-
>
214-
<BlockEditorFormattingToolbar />
215-
<BlockEditorSlashMenu />
216-
</BlockEditor>
223+
<>
224+
<BlockEditor
225+
editor={editor}
226+
initialBlocks={initialBlocks}
227+
onChange={handleEditorChange}
228+
// Disabling the default because we're using a formatting toolbar with the AI option.
229+
formattingToolbar={false}
230+
// Disabling the default because we're using a slash menu with the AI option.
231+
slashMenu={false}
232+
>
233+
<BlockEditorFormattingToolbar />
234+
<BlockEditorSlashMenu />
235+
</BlockEditor>
236+
<BlockEditorErrorOverlay isOpen={isOverlayOpen} />
237+
</>
217238
);
218239
}
219240

apps/web/src/app/(app)/journal/_components/journal-list.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import { useJournlAgentAwareness } from "~/ai/agents/use-journl-agent-awareness"
99
import { useTRPC } from "~/trpc/react";
1010
import {
1111
JournalEntryContent,
12+
JournalEntryEditor,
1213
JournalEntryHeader,
1314
JournalEntryLink,
1415
JournalEntryProvider,
1516
JournalEntryWrapper,
1617
} from "./journal-entry-editor";
17-
import { DynamicJournalEntryEditor } from "./journal-entry-editor.dynamic";
1818
import { JournalEntryLoader } from "./journal-entry-loader";
1919
import { JournalListSkeleton } from "./journal-list-skeleton";
2020

@@ -108,8 +108,8 @@ export function JournalList({
108108
<JournalEntryHeader className="px-13.5" />
109109
</JournalEntryLink>
110110
<JournalEntryContent>
111-
<DynamicJournalEntryEditor
112-
onCreate={(newEntry) => {
111+
<JournalEntryEditor
112+
onCreateAction={(newEntry) => {
113113
queryClient.setQueryData(queryOptions.queryKey, (old) => ({
114114
...old,
115115
pageParams: [...(old?.pageParams ?? [])],

apps/web/src/app/(app)/pages/_components/page-editor.tsx

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import type { BlockTransaction } from "@acme/api";
44
import type { Page } from "@acme/db/schema";
55
import type { PartialBlock } from "@blocknote/core";
66
import { useMutation } from "@tanstack/react-query";
7-
import { useEffect, useRef } from "react";
7+
import { useEffect, useRef, useState } from "react";
88
import { useDebouncedCallback } from "use-debounce";
99
import { useJournlAgentAwareness } from "~/ai/agents/use-journl-agent-awareness";
1010
import { BlockEditor } from "~/components/editor/block-editor";
11+
import { BlockEditorErrorOverlay } from "~/components/editor/block-editor-error-overlay";
1112
import {
1213
BlockEditorFormattingToolbar,
1314
BlockEditorSlashMenu,
@@ -35,12 +36,28 @@ export function PageEditor({
3536
const { rememberEditor, forgetEditor, rememberView } =
3637
useJournlAgentAwareness();
3738
const editor = useBlockEditor({ initialBlocks });
39+
const [isOverlayOpen, setOverlayOpen] = useState(false);
40+
41+
/**
42+
* Handles the error state of the editor.
43+
*
44+
* @privateRemarks
45+
*
46+
* Using replace() to avoid creating a new history entry
47+
*/
48+
function handleError() {
49+
setOverlayOpen(true);
50+
requestAnimationFrame(() => {
51+
location.replace(location.href);
52+
});
53+
}
3854

3955
const { mutate, isPending } = useMutation({
4056
...trpc.pages.saveTransactions.mutationOptions({}),
41-
// ! TODO: When the mutation fails we need to revert the changes to the editor just like Notion does.
42-
// ! To do this we can use `onError` and `editor.undo()`, without calling the transactions. We might have to get creative.
43-
// ! Maybe we can refetch the blocks after an error instead of `undo`?
57+
onError: (error) => {
58+
console.error("[PageEditor] error 👀", error);
59+
handleError();
60+
},
4461
onSuccess: () => {
4562
if (pendingChangesRef.current.length > 0) {
4663
debouncedMutate();
@@ -93,17 +110,20 @@ export function PageEditor({
93110
);
94111

95112
return (
96-
<BlockEditor
97-
editor={editor}
98-
initialBlocks={initialBlocks}
99-
onChange={handleEditorChange}
100-
// Disabling the default because we're using a formatting toolbar with the AI option.
101-
formattingToolbar={false}
102-
// Disabling the default because we're using a slash menu with the AI option.
103-
slashMenu={false}
104-
>
105-
<BlockEditorFormattingToolbar />
106-
<BlockEditorSlashMenu />
107-
</BlockEditor>
113+
<>
114+
<BlockEditor
115+
editor={editor}
116+
initialBlocks={initialBlocks}
117+
onChange={handleEditorChange}
118+
// Disabling the default because we're using a formatting toolbar with the AI option.
119+
formattingToolbar={false}
120+
// Disabling the default because we're using a slash menu with the AI option.
121+
slashMenu={false}
122+
>
123+
<BlockEditorFormattingToolbar />
124+
<BlockEditorSlashMenu />
125+
</BlockEditor>
126+
<BlockEditorErrorOverlay isOpen={isOverlayOpen} />
127+
</>
108128
);
109129
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {
2+
DocumentOverlay,
3+
DocumentOverlayContent,
4+
} from "../ui/document-overlay";
5+
6+
type BlockEditorErrorOverlayProps = {
7+
isOpen: boolean;
8+
};
9+
10+
export function BlockEditorErrorOverlay({
11+
isOpen,
12+
}: BlockEditorErrorOverlayProps) {
13+
return (
14+
<DocumentOverlay
15+
className="bg-background/30 backdrop-blur-md"
16+
isOpen={isOpen}
17+
>
18+
<DocumentOverlayContent className="bg-background/70">
19+
<span className="block font-medium text-muted-foreground text-sm">
20+
Something went wrong while saving your changes.
21+
</span>
22+
</DocumentOverlayContent>
23+
</DocumentOverlay>
24+
);
25+
}

apps/web/src/components/editor/block-editor.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function BlockEditor({
8787
* @privateRemarks
8888
*
8989
* The biggest challenge is the computation of the edges rather than the blocks.
90+
*
9091
* An intuitive approach is to compute the edges for the previous state and the current state and then compute the diff.
9192
* For example, a brute force approach would be to loop over all blocks and detect if their adjacent blocks are different.
9293
* If they are we need to remove the previous edge and insert the new edge.
@@ -115,7 +116,7 @@ export function BlockEditor({
115116
* - Moves are more complex than inserts and deletes because we need to compute edges for the blocks that moved and the blocks that are adjacent to the moved blocks (assuming they didn't move).
116117
*/
117118
function handleEditorChange(currentEditor: EditorPrimitiveOnChangeParams[0]) {
118-
if (!onChange || isEditorAgentProcessing(currentEditor)) return;
119+
if (!onChange || isAgenticEditorChange(currentEditor)) return;
119120

120121
const oldBlocks = getEditorBlocks(previousEditorRef.current);
121122
const currentBlocks = getEditorBlocks(currentEditor.document);
@@ -259,7 +260,7 @@ export function BlockEditor({
259260

260261
// Leaving this here for debugging purposes because this logic is the wild west.
261262
if (debug) {
262-
console.debug("saveTransactions 👀", {
263+
console.debug("[BlockEditor] transactions 👀", {
263264
transactions: transactions.map((t) =>
264265
t.type === "block_remove" || t.type === "block_upsert"
265266
? {
@@ -338,7 +339,7 @@ function getEditorBlocks(blocks: BlockPrimitive[], parent?: BlockPrimitive) {
338339
return flattened;
339340
}
340341

341-
function isEditorAgentProcessing(editor: EditorPrimitive) {
342+
function isAgenticEditorChange(editor: EditorPrimitive) {
342343
const aiExtension = getAIExtension(editor);
343344
const state = aiExtension.store.getState().aiMenuState;
344345

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { ComponentProps } from "react";
2+
import { createPortal } from "react-dom";
3+
import { cn } from "../utils";
4+
5+
type DocumentOverlayProps = ComponentProps<"div"> & {
6+
isOpen: boolean;
7+
};
8+
9+
export function DocumentOverlay({
10+
isOpen,
11+
children,
12+
className,
13+
...rest
14+
}: DocumentOverlayProps) {
15+
if (!isOpen) return null;
16+
return createPortal(
17+
<div
18+
aria-live="polite"
19+
aria-busy="true"
20+
className={cn(
21+
"fixed inset-0 z-[9999] flex items-center justify-center",
22+
className,
23+
)}
24+
{...rest}
25+
>
26+
{children}
27+
</div>,
28+
document.body,
29+
);
30+
}
31+
32+
export function DocumentOverlayContent({
33+
children,
34+
className,
35+
...rest
36+
}: ComponentProps<"div">) {
37+
return (
38+
<div
39+
className={cn(
40+
"rounded-2xl border border-border px-5 py-3 shadow-lg",
41+
className,
42+
)}
43+
{...rest}
44+
>
45+
{children}
46+
</div>
47+
);
48+
}

cspell.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ languageSettings:
3535
allowCompoundWords: false
3636
useGitignore: true
3737
words:
38+
- Agentic
3839
- AISDK
3940
- autohide
4041
- CLIENTVAR

0 commit comments

Comments
 (0)