chunk bug

This commit is contained in:
Pavel committed 2026-09-29 01:48:22 +04:00
1 parent ac7bbd811f
commit 208d34abc7
6 files changed
+99 -18

No files matched your search

+4 -1
View File
@@ -495,7 +495,8 @@ describe('Chunks', () => {
ok: true,
json: async () => ({ chunk_id: 'c1-new' }),
}));
await render({ embedded: true });
const onOpenChunkChange = vi.fn();
await render({ embedded: true, onOpenChunkChange });
await act(async () => tile()!.click());
await act(async () => buttonByText('modals.chunk.edit')!.click());
// After the save, the open position holds nothing (or another chunk).
@@ -509,6 +510,8 @@ describe('Chunks', () => {
expect(container.querySelector('h2')?.textContent).toBe('Rewritten');
expect(container.textContent).toContain('chunkPositionUnplaced');
expect(buttonByLabel('settings.sources.nextChunk')!.disabled).toBe(true);
// An embedding host draws the crumb: still open, but with no number.
expect(onOpenChunkChange).toHaveBeenLastCalledWith('unplaced');
});
it('deleting the only chunk on the last page lands on the page before', async () => {
+14 -3
View File
@@ -35,6 +35,13 @@ import { Pagination } from './ui/pagination';
/** Chunks per page: divisible by 2, 3 and 4 columns, so a page fills the grid. */
const PAGE_SIZE_OPTIONS = [12, 24, 48];
/**
* Where the open chunk is, as reported to an embedding host: its 1-based
* position, 'unplaced' while it is open but its place in the list is unknown
* (a save moved it off every probed position), or null while the grid shows.
*/
export type OpenChunkPosition = number | 'unplaced' | null;
/** Lets the host's crumbs close the open chunk (see TreeBrowser). */
export interface ChunksController {
closeChunk: () => void;
@@ -79,8 +86,8 @@ interface ChunksProps {
* (`onOpenChunkChange`) and close it (`controllerRef`).
*/
embedded?: boolean;
/** The open chunk's position (1-based), or null while the grid shows. */
onOpenChunkChange?: (position: number | null) => void;
/** Where the open chunk is; see {@link OpenChunkPosition}. */
onOpenChunkChange?: (position: OpenChunkPosition) => void;
controllerRef?: React.MutableRefObject<ChunksController | null>;
}
@@ -296,7 +303,11 @@ const Chunks: React.FC<ChunksProps> = ({
closeChunk: () => closeChunk(),
}));
const openChunkPosition = openChunk ? openPosition : null;
const openChunkPosition: OpenChunkPosition = !openChunk
? null
: positionLost
? 'unplaced'
: openPosition;
const onOpenChunkChangeRef = useRef(onOpenChunkChange);
useEffect(() => {
onOpenChunkChangeRef.current = onOpenChunkChange;
@@ -73,13 +73,13 @@ vi.mock('../Chunks', async () => {
default: (props: {
embedded?: boolean;
documentId: string;
onOpenChunkChange?: (position: number | null) => void;
onOpenChunkChange?: (position: number | 'unplaced' | null) => void;
controllerRef?: { current: { closeChunk: () => boolean } | null };
}) => {
const [open, setOpen] = useState(false);
const [open, setOpen] = useState<false | 2 | 'unplaced'>(false);
const { onOpenChunkChange, controllerRef } = props;
useEffect(() => {
onOpenChunkChange?.(open ? 2 : null);
onOpenChunkChange?.(open || null);
}, [open, onOpenChunkChange]);
if (controllerRef) {
controllerRef.current = {
@@ -92,9 +92,12 @@ vi.mock('../Chunks', async () => {
return (
<div data-testid="chunks" data-doc={props.documentId}>
{props.embedded ? 'embedded' : 'page'}
<button type="button" onClick={() => setOpen(true)}>
<button type="button" onClick={() => setOpen(2)}>
OPEN CHUNK
</button>
<button type="button" onClick={() => setOpen('unplaced')}>
LOSE PLACE
</button>
</div>
);
},
@@ -463,6 +466,22 @@ describe('GraphSourceView', () => {
).find((b) => b.textContent === 'Key Accounts')!;
await act(async () => source.click());
expect(crumbs()).toEqual(['settings.sources.label', 'Key Accounts']);
// A saved chunk whose place is unknown keeps an unnumbered crumb, and the
// source crumb still closes it.
await act(async () => buttonByText('LOSE PLACE')!.click());
expect(crumbs()).toEqual([
'settings.sources.label',
'Key Accounts',
'settings.sources.chunkCrumbUnplaced',
]);
const again = Array.from(
container.querySelectorAll<HTMLButtonElement>(
'[data-slot="breadcrumb-link"]',
),
).find((b) => b.textContent === 'Key Accounts')!;
await act(async () => again.click());
expect(crumbs()).toEqual(['settings.sources.label', 'Key Accounts']);
});
it('uses the connector tree for a connector source', async () => {
@@ -13,7 +13,10 @@ import { useSelector } from 'react-redux';
import userService from '../../api/services/userService';
import { selectToken } from '../../preferences/preferenceSlice';
import { formatCount } from '../../utils/dateTimeUtils';
import Chunks, { type ChunksController } from '../Chunks';
import Chunks, {
type ChunksController,
type OpenChunkPosition,
} from '../Chunks';
import ConnectorTree from '../ConnectorTree';
import FileTree from '../FileTree';
import GraphView, { type GraphLoadStatus } from '../GraphView';
@@ -80,9 +83,8 @@ export default function GraphSourceView({
// header shows it while that tab is open.
const [filesCrumbs, setFilesCrumbs] = useState<Crumb[]>([]);
// A flat source's open chunk (its crumb), reported by the chunk list.
const [openChunkPosition, setOpenChunkPosition] = useState<number | null>(
null,
);
const [openChunkPosition, setOpenChunkPosition] =
useState<OpenChunkPosition>(null);
const chunksControllerRef = useRef<ChunksController | null>(null);
useEffect(() => {
@@ -157,7 +159,10 @@ export default function GraphSourceView({
...(openChunkPosition !== null
? [
{
label: t('settings.sources.chunkCrumb', { n: openChunkPosition }),
label:
openChunkPosition === 'unplaced'
? t('settings.sources.chunkCrumbUnplaced')
: t('settings.sources.chunkCrumb', { n: openChunkPosition }),
},
]
: []),
@@ -42,6 +42,10 @@ vi.mock('../../api/services/userService', () => ({
chunks: state.chunks,
}),
})),
updateChunk: vi.fn(async () => ({
ok: true,
json: async () => ({ chunk_id: 'c1' }),
})),
},
}));
@@ -252,6 +256,40 @@ describe('TreeBrowser', () => {
expect(crumbs()).toEqual([SOURCES, 'Contracts', 'readme.md']);
});
it('a saved chunk whose place is unknown keeps an unnumbered crumb', async () => {
state.chunks = [{ doc_id: 'c1', text: '# Rates', metadata: {} }];
await render(NESTED);
await openRow('readme.md');
await act(async () => tile()!.click());
const button = (text: string) =>
Array.from(document.body.querySelectorAll('button')).find(
(el) => el.textContent?.trim() === text,
)!;
await act(async () => button('modals.chunk.edit').click());
// After the save, the probed positions no longer hold the chunk.
state.chunks = [];
const field = document.body.querySelector<HTMLTextAreaElement>(
'[role="dialog"] textarea',
)!;
await act(async () => {
Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
'value',
)!.set!.call(field, '# Rates edited');
field.dispatchEvent(new Event('input', { bubbles: true }));
});
await act(async () => button('modals.chunk.save').click());
expect(crumbs()).toEqual([
SOURCES,
'Contracts',
'readme.md',
'settings.sources.chunkCrumbUnplaced',
]);
await clickCrumb('readme.md');
expect(crumbs()).toEqual([SOURCES, 'Contracts', 'readme.md']);
});
it('a one-file source opens straight on its chunk list', async () => {
await render({ 'report.pdf': { type: 'pdf', display_name: 'Report' } });
expect(
+10 -5
View File
@@ -17,7 +17,10 @@ import { Eye, File, Folder } from 'lucide-react';
import { Button } from '../ui/button';
import { EmptyState } from '../ui/empty-state';
import { useLoaderState } from '../../hooks';
import Chunks, { type ChunksController } from '../Chunks';
import Chunks, {
type ChunksController,
type OpenChunkPosition,
} from '../Chunks';
import PathHeader, { type Crumb } from './PathHeader';
import SourceNavigator from './SourceNavigator';
import SkeletonLoader from '../SkeletonLoader';
@@ -207,9 +210,8 @@ const TreeBrowser: React.FC<TreeBrowserProps> = ({
} | null>(null);
const mountedRef = useRef(true);
// The open file's open chunk (its crumb), reported by Chunks.
const [openChunkPosition, setOpenChunkPosition] = useState<number | null>(
null,
);
const [openChunkPosition, setOpenChunkPosition] =
useState<OpenChunkPosition>(null);
const chunksControllerRef = useRef<ChunksController | null>(null);
useEffect(
@@ -587,7 +589,10 @@ const TreeBrowser: React.FC<TreeBrowserProps> = ({
...(chunkOpen
? [
{
label: t('settings.sources.chunkCrumb', { n: openChunkPosition }),
label:
openChunkPosition === 'unplaced'
? t('settings.sources.chunkCrumbUnplaced')
: t('settings.sources.chunkCrumb', { n: openChunkPosition }),
},
]
: []),