Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 41 additions & 31 deletions src/features/collections/components/CollectionTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
getVisibleFields,
documentsToJson,
getErrorMessage,
resolveSimpleQueryPath,
} from '../../../shared/utils';
import { generateJsQueryFromSimpleParams } from '../../../shared/utils/queryUtils';

Expand Down Expand Up @@ -425,23 +426,6 @@ const CollectionTab: React.FC<CollectionTabProps> = ({
}
}, [viewMode, documents]);

// F5 keyboard shortcut
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'F5') {
e.preventDefault();
if (queryModeRef.current === 'js') {
executeJsQuery();
} else {
loadDocuments();
}
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [executeJsQuery, loadDocuments]);

// Computed values
const allFields = useMemo(() => extractAllFields(documents), [documents]);
const queryFields = useMemo(() => extractQueryableFields(documents), [documents]);
Expand All @@ -460,26 +444,52 @@ const CollectionTab: React.FC<CollectionTabProps> = ({
);

// Event Handlers
const handleSimpleQuery = useCallback(async () => {
const queryPath = resolveSimpleQueryPath(collectionPathInput, documentPath || collectionPath);
if (!queryPath) {
showMessage?.('Enter a valid Firestore collection or document path.', 'error');
return;
}

if (queryPath.shouldOpenPath) {
onOpenCollection?.(queryPath.path, firestoreDatabaseId);
return;
}

await loadDocuments();
}, [
collectionPathInput,
collectionPath,
documentPath,
firestoreDatabaseId,
loadDocuments,
onOpenCollection,
showMessage,
]);

const handleRunQuery = useCallback(async () => {
if (queryMode === 'simple') {
await loadDocuments();
await handleSimpleQuery();
} else {
await executeJsQuery();
}
}, [queryMode, loadDocuments, executeJsQuery]);
}, [queryMode, handleSimpleQuery, executeJsQuery]);

const handleOpenCollectionPath = useCallback(() => {
const nextPath = collectionPathInput.trim().replace(/^\/+|\/+$/g, '');
const segments = nextPath.split('/');

if (!nextPath || segments.some((segment) => !segment)) {
showMessage?.('Enter a valid Firestore collection or document path.', 'error');
return;
}
// F5 follows the same behavior as the Run button, including the edited Simple Query path.
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'F5') return;
event.preventDefault();
if (queryModeRef.current === 'js') {
void executeJsQuery();
} else {
void handleSimpleQuery();
}
};

if (nextPath === (documentPath || collectionPath)) return;
onOpenCollection?.(nextPath, firestoreDatabaseId);
}, [collectionPathInput, collectionPath, documentPath, firestoreDatabaseId, onOpenCollection, showMessage]);
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [executeJsQuery, handleSimpleQuery]);

const handleToggleFavorite = useCallback(() => {
dispatch(
Expand Down Expand Up @@ -757,7 +767,7 @@ const CollectionTab: React.FC<CollectionTabProps> = ({
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
handleOpenCollectionPath();
void handleSimpleQuery();
}
}}
placeholder="Collection path"
Expand Down
26 changes: 25 additions & 1 deletion src/shared/utils/collectionUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { extractQueryableFields } from './collectionUtils';
import { extractQueryableFields, normalizeFirestorePath, resolveSimpleQueryPath } from './collectionUtils';

describe('extractQueryableFields', () => {
it('includes top-level and nested map field paths', () => {
Expand Down Expand Up @@ -34,3 +34,27 @@ describe('extractQueryableFields', () => {
).toEqual(['createdAt', 'location', 'tags']);
});
});

describe('normalizeFirestorePath', () => {
it('preserves a nested document path entered in Simple Query', () => {
expect(normalizeFirestorePath(' /content-plans/plan-id/ads/ad-id/ ')).toBe('content-plans/plan-id/ads/ad-id');
});

it('rejects empty Firestore path segments', () => {
expect(normalizeFirestorePath('content-plans//ads')).toBeNull();
});

it('opens the path currently entered in Simple Query instead of the tab collection', () => {
expect(resolveSimpleQueryPath('content-plans/plan-id/ads/ad-id', 'content-plans')).toEqual({
path: 'content-plans/plan-id/ads/ad-id',
shouldOpenPath: true,
});
});

it('refreshes the current tab when the entered path has not changed', () => {
expect(resolveSimpleQueryPath(' content-plans ', 'content-plans')).toEqual({
path: 'content-plans',
shouldOpenPath: false,
});
});
});
19 changes: 19 additions & 0 deletions src/shared/utils/collectionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,22 @@ export const createDefaultSortConfig = (): SortConfig => ({
field: '',
direction: 'asc',
});

/**
* Normalizes a Firestore collection or document path entered by the user.
* Returns null when the path contains an empty segment.
*/
export const normalizeFirestorePath = (path: string): string | null => {
const normalizedPath = path.trim().replace(/^\/+|\/+$/g, '');
if (!normalizedPath || normalizedPath.split('/').some((segment) => !segment)) return null;
return normalizedPath;
};

/** Decides whether a Simple Query should refresh the current tab or open the entered path. */
export const resolveSimpleQueryPath = (
inputPath: string,
currentPath: string,
): { path: string; shouldOpenPath: boolean } | null => {
const path = normalizeFirestorePath(inputPath);
return path ? { path, shouldOpenPath: path !== currentPath } : null;
};
2 changes: 2 additions & 0 deletions src/shared/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export {
documentsToJson,
createEmptyFilter,
createDefaultSortConfig,
normalizeFirestorePath,
resolveSimpleQueryPath,
} from './collectionUtils';

// Common utilities
Expand Down
Loading