fix(workspace): Enter keeps an empty bullet on its level when siblings follow
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m17s

Outdenting an empty bullet on Enter only happens when no bullet on the
same level follows in the branch; otherwise a new bullet is added below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 22:09:47 +01:00
1 parent b136e5dfad
commit 05355989a8
4 files changed
+50 -3

No files matched your search

@@ -106,6 +106,8 @@ export interface OutlinerBlockProps {
onPasteMultiLines?: (blockId: string, textBefore: string, textAfter: string, rawLines: string[]) => void;
/** Number or letter of a numbered-list item ("1.", "b.", "iii.") */
listMarker?: string;
/** Another bullet on the same level follows: Enter in this empty bullet adds a bullet instead of outdenting */
siblingFollows?: boolean;
lockedBy?: string; // someone else is editing this block
canAppendBelow?: boolean; // the user may edit the note, so Enter in a locked block still adds one below
onResolveConflict?: (blockId: string, accept: boolean) => void;
@@ -155,6 +157,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
registerTextareaRef,
onPasteMultiLines,
listMarker,
siblingFollows = false,
lockedBy,
canAppendBelow,
onResolveConflict,
@@ -961,13 +964,13 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
const minLevel = levelOffset || 0;
if (localContent === '') {
if (Math.max(0, block.level || 0) > minLevel) {
if (Math.max(0, block.level || 0) > minLevel && !siblingFollows) {
flushDebounce(localContent);
onIndentBlockRef.current(block.id, -1);
return;
}
// Enter in an empty numbered item at the top ends the list
if (block.type === 'numbered') {
// Enter in an empty last numbered item at the top ends the list
if (block.type === 'numbered' && !siblingFollows) {
flushDebounce('', { type: 'text' });
return;
}
@@ -244,6 +244,21 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
const listLabels = useMemo(() => numberedLabels(blocks), [blocks]);
// Whether another bullet on the same level follows (before the branch ends); an empty bullet
// is then not outdented by Enter
const siblingFollows = useCallback(
(index: number) => {
const level = blocks[index]?.level || 0;
for (let i = index + 1; i < blocks.length; i++) {
const l = blocks[i].level || 0;
if (l < level) return false;
if (l === level) return true;
}
return false;
},
[blocks]
);
const handleCreateBlockBelow = useCallback(
(afterBlockId: string, newContent: string = '') => {
if (!canEdit) return;
@@ -617,6 +632,7 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
block={block}
index={index}
isLast={isLast}
siblingFollows={siblingFollows(index)}
maxAllowedLevel={maxAllowedLevel}
hasChildren={hasChildren}
isFocused={focusedBlockId === block.id}
@@ -207,6 +207,33 @@ describe('slash command menu and outliner fixes', () => {
expect(onCreateBelow).toHaveBeenCalledWith('b1', '');
});
it('keeps an empty indented bullet on its level when another bullet on that level follows', () => {
const onIndent = vi.fn();
const onCreateBelow = vi.fn();
const { container } = render(
<OutlinerBlock
docId="d"
block={{ id: 'b1', type: 'bullet', content: '', level: 1 } as DocumentBlock}
index={0}
isLast={false}
siblingFollows
maxAllowedLevel={2}
hasChildren={false}
isFocused
canEdit
onUpdateBlock={() => {}}
onCreateBlockBelow={onCreateBelow}
onDeleteBlock={() => {}}
onIndentBlock={onIndent}
onNavigateFocus={() => {}}
registerTextareaRef={() => {}}
/>
);
fireEvent.keyDown(container.querySelector('textarea') as HTMLTextAreaElement, { key: 'Enter' });
expect(onIndent).not.toHaveBeenCalled();
expect(onCreateBelow).toHaveBeenCalledWith('b1', '');
});
it('inserts a bare URL for /link with a web address in an empty bullet, and [Text](url) otherwise', () => {
const updates: DocumentBlock[] = [];
const block = { id: 'b1', type: 'bullet', content: '', level: 0 } as DocumentBlock;