refactor: reorganize features/shared directory for better maintainability (#730)
Some checks failed
Continuous Integration / Frontend Tests (React + Vitest) (push) Has been cancelled
Continuous Integration / Backend Tests (Python + pytest) (push) Has been cancelled
Continuous Integration / Docker Build Tests (agents) (push) Has been cancelled
Continuous Integration / Docker Build Tests (frontend) (push) Has been cancelled
Continuous Integration / Docker Build Tests (mcp) (push) Has been cancelled
Continuous Integration / Docker Build Tests (server) (push) Has been cancelled
Continuous Integration / Test Results Summary (push) Has been cancelled
Some checks failed
Continuous Integration / Frontend Tests (React + Vitest) (push) Has been cancelled
Continuous Integration / Backend Tests (Python + pytest) (push) Has been cancelled
Continuous Integration / Docker Build Tests (agents) (push) Has been cancelled
Continuous Integration / Docker Build Tests (frontend) (push) Has been cancelled
Continuous Integration / Docker Build Tests (mcp) (push) Has been cancelled
Continuous Integration / Docker Build Tests (server) (push) Has been cancelled
Continuous Integration / Test Results Summary (push) Has been cancelled
* refactor: reorganize features/shared directory structure - Created organized subdirectories for better code organization: - api/ - API clients and HTTP utilities (renamed apiWithEtag.ts to apiClient.ts) - config/ - Configuration files (queryClient, queryPatterns) - types/ - Shared type definitions (errors) - utils/ - Pure utility functions (optimistic, clipboard) - hooks/ - Shared React hooks (already existed) - Updated all import paths across the codebase (~40+ files) - Updated all AI documentation in PRPs/ai_docs/ to reflect new structure - All tests passing, build successful, no functional changes This improves maintainability and follows vertical slice architecture patterns. Co-Authored-By: Claude <noreply@anthropic.com> * fix: address PR review comments and code improvements - Update imports to use @/features alias path for optimistic utils - Fix optimistic upload item replacement by matching on source_id instead of id - Clean up test suite naming and remove meta-terms from comments - Only set Content-Type header on requests with body - Add explicit TypeScript typing to useProjectFeatures hook - Complete Phase 4 improvements with proper query typing * fix: address additional PR review feedback - Clear feature queries when deleting project to prevent cache memory leaks - Update KnowledgeCard comments to follow documentation guidelines - Add explanatory comment for accessibility pattern in KnowledgeCard --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d3a5c3311a
commit
63a92cf7d7
@ -198,7 +198,7 @@ Database values used directly - no mapping layers:
|
||||
- Operation statuses: `"pending"`, `"processing"`, `"completed"`, `"failed"`
|
||||
|
||||
### Time Constants
|
||||
**Location**: `archon-ui-main/src/features/shared/queryPatterns.ts`
|
||||
**Location**: `archon-ui-main/src/features/shared/config/queryPatterns.ts`
|
||||
- `STALE_TIMES.instant` - 0ms
|
||||
- `STALE_TIMES.realtime` - 3 seconds
|
||||
- `STALE_TIMES.frequent` - 5 seconds
|
||||
|
||||
@ -88,8 +88,8 @@ Pattern: `{METHOD} /api/{resource}/{id?}/{sub-resource?}`
|
||||
|
||||
### Data Fetching
|
||||
**Core**: TanStack Query v5
|
||||
**Configuration**: `archon-ui-main/src/features/shared/queryClient.ts`
|
||||
**Patterns**: `archon-ui-main/src/features/shared/queryPatterns.ts`
|
||||
**Configuration**: `archon-ui-main/src/features/shared/config/queryClient.ts`
|
||||
**Patterns**: `archon-ui-main/src/features/shared/config/queryPatterns.ts`
|
||||
|
||||
### State Management
|
||||
- **Server State**: TanStack Query
|
||||
@ -139,7 +139,7 @@ TanStack Query is the single source of truth. No separate state management neede
|
||||
No translation layers. Database values (e.g., `"todo"`, `"doing"`) used directly in UI.
|
||||
|
||||
### Browser-Native Caching
|
||||
ETags handled by browser, not JavaScript. See `archon-ui-main/src/features/shared/apiWithEtag.ts`.
|
||||
ETags handled by browser, not JavaScript. See `archon-ui-main/src/features/shared/api/apiClient.ts`.
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ Archon uses **TanStack Query v5** for all data fetching, caching, and synchroniz
|
||||
|
||||
### 1. Query Client Configuration
|
||||
|
||||
**Location**: `archon-ui-main/src/features/shared/queryClient.ts`
|
||||
**Location**: `archon-ui-main/src/features/shared/config/queryClient.ts`
|
||||
|
||||
Centralized QueryClient with:
|
||||
|
||||
@ -30,7 +30,7 @@ Visibility-aware polling that:
|
||||
|
||||
### 3. Query Patterns
|
||||
|
||||
**Location**: `archon-ui-main/src/features/shared/queryPatterns.ts`
|
||||
**Location**: `archon-ui-main/src/features/shared/config/queryPatterns.ts`
|
||||
|
||||
Shared constants:
|
||||
|
||||
@ -64,7 +64,7 @@ Standard pattern across all features:
|
||||
|
||||
### ETag Support
|
||||
|
||||
**Location**: `archon-ui-main/src/features/shared/apiWithEtag.ts`
|
||||
**Location**: `archon-ui-main/src/features/shared/api/apiClient.ts`
|
||||
|
||||
ETag implementation:
|
||||
|
||||
@ -83,7 +83,7 @@ Backend endpoints follow RESTful patterns:
|
||||
|
||||
## Optimistic Updates
|
||||
|
||||
**Utilities**: `archon-ui-main/src/features/shared/optimistic.ts`
|
||||
**Utilities**: `archon-ui-main/src/features/shared/utils/optimistic.ts`
|
||||
|
||||
All mutations use nanoid-based optimistic updates:
|
||||
|
||||
@ -105,7 +105,7 @@ Polling intervals are defined in each feature's query hooks. See actual implemen
|
||||
- **Progress**: `archon-ui-main/src/features/progress/hooks/useProgressQueries.ts`
|
||||
- **MCP**: `archon-ui-main/src/features/mcp/hooks/useMcpQueries.ts`
|
||||
|
||||
Standard intervals from `archon-ui-main/src/features/shared/queryPatterns.ts`:
|
||||
Standard intervals from `archon-ui-main/src/features/shared/config/queryPatterns.ts`:
|
||||
- `STALE_TIMES.instant`: 0ms (always fresh)
|
||||
- `STALE_TIMES.frequent`: 5 seconds (frequently changing data)
|
||||
- `STALE_TIMES.normal`: 30 seconds (standard cache)
|
||||
|
||||
@ -17,7 +17,7 @@ The backend generates ETags for API responses:
|
||||
- Returns `304 Not Modified` when ETags match
|
||||
|
||||
### Frontend Handling
|
||||
**Location**: `archon-ui-main/src/features/shared/apiWithEtag.ts`
|
||||
**Location**: `archon-ui-main/src/features/shared/api/apiClient.ts`
|
||||
|
||||
The frontend relies on browser-native HTTP caching:
|
||||
- Browser automatically sends `If-None-Match` headers with cached ETags
|
||||
@ -28,7 +28,7 @@ The frontend relies on browser-native HTTP caching:
|
||||
#### Browser vs Non-Browser Behavior
|
||||
- **Standard Browsers**: Per the Fetch spec, a 304 response freshens the HTTP cache and returns the cached body to JavaScript
|
||||
- **Non-Browser Runtimes** (React Native, custom fetch): May surface 304 with empty body to JavaScript
|
||||
- **Client Fallback**: The `apiWithEtag.ts` implementation handles both scenarios, ensuring consistent behavior across environments
|
||||
- **Client Fallback**: The `apiClient.ts` implementation handles both scenarios, ensuring consistent behavior across environments
|
||||
|
||||
## Implementation Details
|
||||
|
||||
@ -81,8 +81,8 @@ Unlike previous implementations, the current approach:
|
||||
|
||||
### Configuration
|
||||
Cache behavior is controlled through TanStack Query's `staleTime`:
|
||||
- See `archon-ui-main/src/features/shared/queryPatterns.ts` for standard times
|
||||
- See `archon-ui-main/src/features/shared/queryClient.ts` for global configuration
|
||||
- See `archon-ui-main/src/features/shared/config/queryPatterns.ts` for standard times
|
||||
- See `archon-ui-main/src/features/shared/config/queryClient.ts` for global configuration
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
@ -100,7 +100,7 @@ Cache behavior is controlled through TanStack Query's `staleTime`:
|
||||
|
||||
### Core Implementation
|
||||
- **Backend Utilities**: `python/src/server/utils/etag_utils.py`
|
||||
- **Frontend Client**: `archon-ui-main/src/features/shared/apiWithEtag.ts`
|
||||
- **Frontend Client**: `archon-ui-main/src/features/shared/api/apiClient.ts`
|
||||
- **Tests**: `python/tests/server/utils/test_etag_utils.py`
|
||||
|
||||
### Usage Examples
|
||||
|
||||
@ -5,7 +5,7 @@ This guide documents the standardized patterns for using TanStack Query v5 in th
|
||||
## Core Principles
|
||||
|
||||
1. **Feature Ownership**: Each feature owns its query keys in `{feature}/hooks/use{Feature}Queries.ts`
|
||||
2. **Consistent Patterns**: Always use shared patterns from `shared/queryPatterns.ts`
|
||||
2. **Consistent Patterns**: Always use shared patterns from `shared/config/queryPatterns.ts`
|
||||
3. **No Hardcoded Values**: Never hardcode stale times or disabled keys
|
||||
4. **Mirror Backend API**: Query keys should exactly match backend API structure
|
||||
|
||||
@ -49,7 +49,7 @@ export const taskKeys = {
|
||||
### Import Required Patterns
|
||||
|
||||
```typescript
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "@/features/shared/queryPatterns";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "@/features/shared/config/queryPatterns";
|
||||
```
|
||||
|
||||
### Disabled Queries
|
||||
@ -106,7 +106,7 @@ export function useFeatureDetail(id: string | undefined) {
|
||||
## Mutations with Optimistic Updates
|
||||
|
||||
```typescript
|
||||
import { createOptimisticEntity, replaceOptimisticEntity } from "@/features/shared/optimistic";
|
||||
import { createOptimisticEntity, replaceOptimisticEntity } from "@/features/shared/utils/optimistic";
|
||||
|
||||
export function useCreateFeature() {
|
||||
const queryClient = useQueryClient();
|
||||
@ -161,7 +161,7 @@ vi.mock("../../services", () => ({
|
||||
}));
|
||||
|
||||
// Mock shared patterns with ALL values
|
||||
vi.mock("../../../shared/queryPatterns", () => ({
|
||||
vi.mock("../../../shared/config/queryPatterns", () => ({
|
||||
DISABLED_QUERY_KEY: ["disabled"] as const,
|
||||
STALE_TIMES: {
|
||||
instant: 0,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
## Core Architecture
|
||||
|
||||
### Shared Utilities Module
|
||||
**Location**: `src/features/shared/optimistic.ts`
|
||||
**Location**: `src/features/shared/utils/optimistic.ts`
|
||||
|
||||
Provides type-safe utilities for managing optimistic state across all features:
|
||||
- `createOptimisticId()` - Generates stable UUIDs using nanoid
|
||||
@ -73,13 +73,13 @@ Reusable component showing:
|
||||
- Uses `createOptimisticId()` directly for progress tracking
|
||||
|
||||
### Toasts
|
||||
- **Location**: `src/features/ui/hooks/useToast.ts:43`
|
||||
- **Location**: `src/features/shared/hooks/useToast.ts:43`
|
||||
- Uses `createOptimisticId()` for unique toast IDs
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
**Location**: `src/features/shared/optimistic.test.ts`
|
||||
**Location**: `src/features/shared/utils/tests/optimistic.test.ts`
|
||||
|
||||
Covers all utility functions with 8 test cases:
|
||||
- ID uniqueness and format validation
|
||||
|
||||
@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { queryClient } from './features/shared/queryClient';
|
||||
import { queryClient } from './features/shared/config/queryClient';
|
||||
import { KnowledgeBasePage } from './pages/KnowledgeBasePage';
|
||||
import { SettingsPage } from './pages/SettingsPage';
|
||||
import { MCPPage } from './pages/MCPPage';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { callAPIWithETag } from "../../../features/shared/apiWithEtag";
|
||||
import { createRetryLogic, STALE_TIMES } from "../../../features/shared/queryPatterns";
|
||||
import { callAPIWithETag } from "../../../features/shared/api/apiClient";
|
||||
import { createRetryLogic, STALE_TIMES } from "../../../features/shared/config/queryPatterns";
|
||||
import type { HealthResponse } from "../types";
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Enhanced Knowledge Card Component
|
||||
* Individual knowledge item card with excellent UX and inline progress
|
||||
* Knowledge Card component
|
||||
* Displays a knowledge item with inline progress and status UI
|
||||
* Following the pattern from ProjectCard
|
||||
*/
|
||||
|
||||
@ -10,7 +10,7 @@ import { Clock, Code, ExternalLink, File, FileText, Globe } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { KnowledgeCardProgress } from "../../progress/components/KnowledgeCardProgress";
|
||||
import type { ActiveOperation } from "../../progress/types";
|
||||
import { isOptimistic } from "../../shared/optimistic";
|
||||
import { isOptimistic } from "@/features/shared/utils/optimistic";
|
||||
import { StatPill } from "../../ui/primitives";
|
||||
import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator";
|
||||
import { cn } from "../../ui/primitives/styles";
|
||||
@ -144,6 +144,7 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (buttons, links) - using div to avoid invalid HTML nesting
|
||||
<motion.div
|
||||
className="relative group cursor-pointer"
|
||||
role="button"
|
||||
|
||||
@ -5,13 +5,13 @@
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
import { createOptimisticEntity, createOptimisticId } from "@/features/shared/optimistic";
|
||||
import { useSmartPolling } from "@/features/shared/hooks";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
import { createOptimisticEntity, createOptimisticId } from "@/features/shared/utils/optimistic";
|
||||
import { useActiveOperations } from "../../progress/hooks";
|
||||
import { progressKeys } from "../../progress/hooks/useProgressQueries";
|
||||
import type { ActiveOperation, ActiveOperationsResponse } from "../../progress/types";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/queryPatterns";
|
||||
import { useSmartPolling } from "@/features/shared/hooks";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/config/queryPatterns";
|
||||
import { knowledgeService } from "../services";
|
||||
import type {
|
||||
CrawlRequest,
|
||||
@ -170,7 +170,6 @@ export function useCrawlUrl() {
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
} as Omit<KnowledgeItem, "id">);
|
||||
const tempItemId = optimisticItem.id;
|
||||
|
||||
// Update all summaries caches with optimistic data, respecting each cache's filter
|
||||
const entries = queryClient.getQueriesData<KnowledgeItemsResponse>({
|
||||
@ -229,7 +228,7 @@ export function useCrawlUrl() {
|
||||
});
|
||||
|
||||
// Return context for rollback and replacement
|
||||
return { previousSummaries, previousOperations, tempProgressId, tempItemId };
|
||||
return { previousSummaries, previousOperations, tempProgressId };
|
||||
},
|
||||
onSuccess: (response, _variables, context) => {
|
||||
// Replace temporary IDs with real ones from the server
|
||||
@ -313,7 +312,6 @@ export function useUploadDocument() {
|
||||
previousSummaries?: Array<[readonly unknown[], KnowledgeItemsResponse | undefined]>;
|
||||
previousOperations?: ActiveOperationsResponse;
|
||||
tempProgressId: string;
|
||||
tempItemId: string;
|
||||
}
|
||||
>({
|
||||
mutationFn: ({ file, metadata }: { file: File; metadata: UploadMetadata }) =>
|
||||
@ -352,7 +350,6 @@ export function useUploadDocument() {
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
} as Omit<KnowledgeItem, "id">);
|
||||
const tempItemId = optimisticItem.id;
|
||||
|
||||
// Respect each cache's filter (knowledge_type, tags, etc.)
|
||||
const entries = queryClient.getQueriesData<KnowledgeItemsResponse>({
|
||||
@ -410,7 +407,7 @@ export function useUploadDocument() {
|
||||
};
|
||||
});
|
||||
|
||||
return { previousSummaries, previousOperations, tempProgressId, tempItemId };
|
||||
return { previousSummaries, previousOperations, tempProgressId };
|
||||
},
|
||||
onSuccess: (response, _variables, context) => {
|
||||
// Replace temporary IDs with real ones from the server
|
||||
@ -421,7 +418,7 @@ export function useUploadDocument() {
|
||||
return {
|
||||
...old,
|
||||
items: old.items.map((item) => {
|
||||
if (item.id === context.tempItemId) {
|
||||
if (item.source_id === context.tempProgressId) {
|
||||
return {
|
||||
...item,
|
||||
source_id: response.progressId,
|
||||
|
||||
@ -4,13 +4,13 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { copyToClipboard } from "../../../shared/utils/clipboard";
|
||||
import { InspectorDialog, InspectorDialogContent, InspectorDialogTitle } from "../../../ui/primitives";
|
||||
import type { CodeExample, DocumentChunk, InspectorSelectedItem, KnowledgeItem } from "../../types";
|
||||
import { useInspectorPagination } from "../hooks/useInspectorPagination";
|
||||
import { ContentViewer } from "./ContentViewer";
|
||||
import { InspectorHeader } from "./InspectorHeader";
|
||||
import { InspectorSidebar } from "./InspectorSidebar";
|
||||
import { copyToClipboard } from "../../../shared/utils/clipboard";
|
||||
|
||||
interface KnowledgeInspectorProps {
|
||||
item: KnowledgeItem;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { STALE_TIMES } from "@/features/shared/queryPatterns";
|
||||
import { STALE_TIMES } from "@/features/shared/config/queryPatterns";
|
||||
import { knowledgeKeys } from "../../hooks/useKnowledgeQueries";
|
||||
import { knowledgeService } from "../../services";
|
||||
import type { ChunksResponse, CodeExample, CodeExamplesResponse, DocumentChunk } from "../../types";
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
* Handles all knowledge-related API operations using TanStack Query patterns
|
||||
*/
|
||||
|
||||
import { callAPIWithETag } from "../../shared/apiWithEtag";
|
||||
import { APIServiceError } from "../../shared/errors";
|
||||
import { callAPIWithETag } from "../../shared/api/apiClient";
|
||||
import { APIServiceError } from "../../shared/types/errors";
|
||||
import type {
|
||||
ChunksResponse,
|
||||
CodeExamplesResponse,
|
||||
|
||||
@ -1,70 +1,70 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseProviderError, getProviderErrorMessage, type ProviderError } from '../providerErrorHandler';
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getProviderErrorMessage, type ProviderError, parseProviderError } from "../providerErrorHandler";
|
||||
|
||||
describe('providerErrorHandler', () => {
|
||||
describe('parseProviderError', () => {
|
||||
it('should handle basic Error objects', () => {
|
||||
const error = new Error('Basic error message');
|
||||
describe("providerErrorHandler", () => {
|
||||
describe("parseProviderError", () => {
|
||||
it("should handle basic Error objects", () => {
|
||||
const error = new Error("Basic error message");
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.message).toBe('Basic error message');
|
||||
expect(result.message).toBe("Basic error message");
|
||||
expect(result.isProviderError).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle errors with statusCode property', () => {
|
||||
const error = { statusCode: 401, message: 'Unauthorized' };
|
||||
it("should handle errors with statusCode property", () => {
|
||||
const error = { statusCode: 401, message: "Unauthorized" };
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
expect(result.message).toBe('Unauthorized');
|
||||
expect(result.message).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it('should handle errors with status property', () => {
|
||||
const error = { status: 429, message: 'Rate limited' };
|
||||
it("should handle errors with status property", () => {
|
||||
const error = { status: 429, message: "Rate limited" };
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.statusCode).toBe(429);
|
||||
expect(result.message).toBe('Rate limited');
|
||||
expect(result.message).toBe("Rate limited");
|
||||
});
|
||||
|
||||
it('should prioritize statusCode over status when both are present', () => {
|
||||
const error = { statusCode: 401, status: 429, message: 'Auth error' };
|
||||
it("should prioritize statusCode over status when both are present", () => {
|
||||
const error = { statusCode: 401, status: 429, message: "Auth error" };
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('should parse structured provider errors from backend', () => {
|
||||
it("should parse structured provider errors from backend", () => {
|
||||
const error = {
|
||||
message: JSON.stringify({
|
||||
detail: {
|
||||
error_type: 'authentication_failed',
|
||||
provider: 'OpenAI',
|
||||
message: 'Invalid API key'
|
||||
}
|
||||
})
|
||||
error_type: "authentication_failed",
|
||||
provider: "OpenAI",
|
||||
message: "Invalid API key",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.isProviderError).toBe(true);
|
||||
expect(result.provider).toBe('OpenAI');
|
||||
expect(result.errorType).toBe('authentication_failed');
|
||||
expect(result.message).toBe('Invalid API key');
|
||||
expect(result.provider).toBe("OpenAI");
|
||||
expect(result.errorType).toBe("authentication_failed");
|
||||
expect(result.message).toBe("Invalid API key");
|
||||
});
|
||||
|
||||
it('should handle malformed JSON in message gracefully', () => {
|
||||
it("should handle malformed JSON in message gracefully", () => {
|
||||
const error = {
|
||||
message: 'invalid json { detail'
|
||||
message: "invalid json { detail",
|
||||
};
|
||||
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.isProviderError).toBeUndefined();
|
||||
expect(result.message).toBe('invalid json { detail');
|
||||
expect(result.message).toBe("invalid json { detail");
|
||||
});
|
||||
|
||||
it('should handle null and undefined inputs safely', () => {
|
||||
it("should handle null and undefined inputs safely", () => {
|
||||
expect(() => parseProviderError(null)).not.toThrow();
|
||||
expect(() => parseProviderError(undefined)).not.toThrow();
|
||||
|
||||
@ -75,7 +75,7 @@ describe('providerErrorHandler', () => {
|
||||
expect(undefinedResult).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle empty objects', () => {
|
||||
it("should handle empty objects", () => {
|
||||
const result = parseProviderError({});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
@ -83,171 +83,171 @@ describe('providerErrorHandler', () => {
|
||||
expect(result.isProviderError).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle primitive values', () => {
|
||||
expect(() => parseProviderError('string error')).not.toThrow();
|
||||
it("should handle primitive values", () => {
|
||||
expect(() => parseProviderError("string error")).not.toThrow();
|
||||
expect(() => parseProviderError(42)).not.toThrow();
|
||||
expect(() => parseProviderError(true)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle structured errors without provider field', () => {
|
||||
it("should handle structured errors without provider field", () => {
|
||||
const error = {
|
||||
message: JSON.stringify({
|
||||
detail: {
|
||||
error_type: 'quota_exhausted',
|
||||
message: 'Usage limit exceeded'
|
||||
}
|
||||
})
|
||||
error_type: "quota_exhausted",
|
||||
message: "Usage limit exceeded",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.isProviderError).toBe(true);
|
||||
expect(result.provider).toBe('LLM'); // Default fallback
|
||||
expect(result.errorType).toBe('quota_exhausted');
|
||||
expect(result.message).toBe('Usage limit exceeded');
|
||||
expect(result.provider).toBe("LLM"); // Default fallback
|
||||
expect(result.errorType).toBe("quota_exhausted");
|
||||
expect(result.message).toBe("Usage limit exceeded");
|
||||
});
|
||||
|
||||
it('should handle partial structured errors', () => {
|
||||
it("should handle partial structured errors", () => {
|
||||
const error = {
|
||||
message: JSON.stringify({
|
||||
detail: {
|
||||
error_type: 'rate_limit'
|
||||
error_type: "rate_limit",
|
||||
// Missing message field
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const result = parseProviderError(error);
|
||||
|
||||
expect(result.isProviderError).toBe(true);
|
||||
expect(result.errorType).toBe('rate_limit');
|
||||
expect(result.errorType).toBe("rate_limit");
|
||||
expect(result.message).toBe(error.message); // Falls back to original message
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderErrorMessage', () => {
|
||||
it('should return user-friendly message for authentication_failed', () => {
|
||||
describe("getProviderErrorMessage", () => {
|
||||
it("should return user-friendly message for authentication_failed", () => {
|
||||
const error: ProviderError = {
|
||||
name: 'Error',
|
||||
message: 'Auth failed',
|
||||
name: "Error",
|
||||
message: "Auth failed",
|
||||
isProviderError: true,
|
||||
provider: 'OpenAI',
|
||||
errorType: 'authentication_failed'
|
||||
provider: "OpenAI",
|
||||
errorType: "authentication_failed",
|
||||
};
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('Please verify your OpenAI API key in Settings.');
|
||||
expect(result).toBe("Please verify your OpenAI API key in Settings.");
|
||||
});
|
||||
|
||||
it('should return user-friendly message for quota_exhausted', () => {
|
||||
it("should return user-friendly message for quota_exhausted", () => {
|
||||
const error: ProviderError = {
|
||||
name: 'Error',
|
||||
message: 'Quota exceeded',
|
||||
name: "Error",
|
||||
message: "Quota exceeded",
|
||||
isProviderError: true,
|
||||
provider: 'Google AI',
|
||||
errorType: 'quota_exhausted'
|
||||
provider: "Google AI",
|
||||
errorType: "quota_exhausted",
|
||||
};
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('Google AI quota exhausted. Please check your billing settings.');
|
||||
expect(result).toBe("Google AI quota exhausted. Please check your billing settings.");
|
||||
});
|
||||
|
||||
it('should return user-friendly message for rate_limit', () => {
|
||||
it("should return user-friendly message for rate_limit", () => {
|
||||
const error: ProviderError = {
|
||||
name: 'Error',
|
||||
message: 'Rate limited',
|
||||
name: "Error",
|
||||
message: "Rate limited",
|
||||
isProviderError: true,
|
||||
provider: 'Anthropic',
|
||||
errorType: 'rate_limit'
|
||||
provider: "Anthropic",
|
||||
errorType: "rate_limit",
|
||||
};
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('Anthropic rate limit exceeded. Please wait and try again.');
|
||||
expect(result).toBe("Anthropic rate limit exceeded. Please wait and try again.");
|
||||
});
|
||||
|
||||
it('should return generic provider message for unknown error types', () => {
|
||||
it("should return generic provider message for unknown error types", () => {
|
||||
const error: ProviderError = {
|
||||
name: 'Error',
|
||||
message: 'Unknown error',
|
||||
name: "Error",
|
||||
message: "Unknown error",
|
||||
isProviderError: true,
|
||||
provider: 'OpenAI',
|
||||
errorType: 'unknown_error'
|
||||
provider: "OpenAI",
|
||||
errorType: "unknown_error",
|
||||
};
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('OpenAI API error. Please check your configuration.');
|
||||
expect(result).toBe("OpenAI API error. Please check your configuration.");
|
||||
});
|
||||
|
||||
it('should use default provider when provider is missing', () => {
|
||||
it("should use default provider when provider is missing", () => {
|
||||
const error: ProviderError = {
|
||||
name: 'Error',
|
||||
message: 'Auth failed',
|
||||
name: "Error",
|
||||
message: "Auth failed",
|
||||
isProviderError: true,
|
||||
errorType: 'authentication_failed'
|
||||
errorType: "authentication_failed",
|
||||
};
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('Please verify your LLM API key in Settings.');
|
||||
expect(result).toBe("Please verify your LLM API key in Settings.");
|
||||
});
|
||||
|
||||
it('should handle 401 status code for non-provider errors', () => {
|
||||
const error = { statusCode: 401, message: 'Unauthorized' };
|
||||
it("should handle 401 status code for non-provider errors", () => {
|
||||
const error = { statusCode: 401, message: "Unauthorized" };
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('Please verify your API key in Settings.');
|
||||
expect(result).toBe("Please verify your API key in Settings.");
|
||||
});
|
||||
|
||||
it('should return original message for non-provider errors', () => {
|
||||
const error = new Error('Network connection failed');
|
||||
it("should return original message for non-provider errors", () => {
|
||||
const error = new Error("Network connection failed");
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('Network connection failed');
|
||||
expect(result).toBe("Network connection failed");
|
||||
});
|
||||
|
||||
it('should return default message when no message is available', () => {
|
||||
it("should return default message when no message is available", () => {
|
||||
const error = {};
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('An error occurred.');
|
||||
expect(result).toBe("An error occurred.");
|
||||
});
|
||||
|
||||
it('should handle complex error objects with structured backend response', () => {
|
||||
it("should handle complex error objects with structured backend response", () => {
|
||||
const backendError = {
|
||||
statusCode: 400,
|
||||
message: JSON.stringify({
|
||||
detail: {
|
||||
error_type: 'authentication_failed',
|
||||
provider: 'OpenAI',
|
||||
message: 'API key invalid or expired'
|
||||
}
|
||||
})
|
||||
error_type: "authentication_failed",
|
||||
provider: "OpenAI",
|
||||
message: "API key invalid or expired",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const result = getProviderErrorMessage(backendError);
|
||||
expect(result).toBe('Please verify your OpenAI API key in Settings.');
|
||||
expect(result).toBe("Please verify your OpenAI API key in Settings.");
|
||||
});
|
||||
|
||||
it('should handle edge case: message contains "detail" but is not JSON', () => {
|
||||
const error = {
|
||||
message: 'Error detail: something went wrong'
|
||||
message: "Error detail: something went wrong",
|
||||
};
|
||||
|
||||
const result = getProviderErrorMessage(error);
|
||||
expect(result).toBe('Error detail: something went wrong');
|
||||
expect(result).toBe("Error detail: something went wrong");
|
||||
});
|
||||
|
||||
it('should handle null and undefined gracefully', () => {
|
||||
expect(getProviderErrorMessage(null)).toBe('An error occurred.');
|
||||
expect(getProviderErrorMessage(undefined)).toBe('An error occurred.');
|
||||
it("should handle null and undefined gracefully", () => {
|
||||
expect(getProviderErrorMessage(null)).toBe("An error occurred.");
|
||||
expect(getProviderErrorMessage(undefined)).toBe("An error occurred.");
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypeScript strict mode compliance', () => {
|
||||
it('should handle type-safe property access', () => {
|
||||
describe("TypeScript strict mode compliance", () => {
|
||||
it("should handle type-safe property access", () => {
|
||||
// Test that our type guards work properly
|
||||
const errorWithStatus = { statusCode: 500 };
|
||||
const errorWithMessage = { message: 'test' };
|
||||
const errorWithBoth = { statusCode: 401, message: 'unauthorized' };
|
||||
const errorWithMessage = { message: "test" };
|
||||
const errorWithBoth = { statusCode: 401, message: "unauthorized" };
|
||||
|
||||
// These should not throw TypeScript errors and should work correctly
|
||||
expect(() => parseProviderError(errorWithStatus)).not.toThrow();
|
||||
@ -259,13 +259,13 @@ describe('providerErrorHandler', () => {
|
||||
const result3 = parseProviderError(errorWithBoth);
|
||||
|
||||
expect(result1.statusCode).toBe(500);
|
||||
expect(result2.message).toBe('test');
|
||||
expect(result2.message).toBe("test");
|
||||
expect(result3.statusCode).toBe(401);
|
||||
expect(result3.message).toBe('unauthorized');
|
||||
expect(result3.message).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it('should handle objects without expected properties safely', () => {
|
||||
const objectWithoutStatus = { someOtherProperty: 'value' };
|
||||
it("should handle objects without expected properties safely", () => {
|
||||
const objectWithoutStatus = { someOtherProperty: "value" };
|
||||
const objectWithoutMessage = { anotherProperty: 42 };
|
||||
|
||||
expect(() => parseProviderError(objectWithoutStatus)).not.toThrow();
|
||||
@ -278,4 +278,4 @@ describe('providerErrorHandler', () => {
|
||||
expect(result2.message).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
import { CrawlingProgress } from "../../progress/components/CrawlingProgress";
|
||||
import type { ActiveOperation } from "../../progress/types";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
import { AddKnowledgeDialog } from "../components/AddKnowledgeDialog";
|
||||
import { KnowledgeHeader } from "../components/KnowledgeHeader";
|
||||
import { KnowledgeList } from "../components/KnowledgeList";
|
||||
|
||||
@ -2,9 +2,9 @@ import { Copy, ExternalLink } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { useToast } from "@/features/shared/hooks";
|
||||
import { copyToClipboard } from "../../shared/utils/clipboard";
|
||||
import { Button, cn, glassmorphism, Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives";
|
||||
import type { McpServerConfig, McpServerStatus, SupportedIDE } from "../types";
|
||||
import { copyToClipboard } from "../../shared/utils/clipboard";
|
||||
|
||||
interface McpConfigSectionProps {
|
||||
config?: McpServerConfig;
|
||||
@ -324,7 +324,8 @@ export const McpConfigSection: React.FC<McpConfigSectionProps> = ({ config, stat
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||
<span className="font-semibold">Platform Note:</span> The configuration below shows{" "}
|
||||
{navigator.platform.toLowerCase().includes("win") ? "Windows" : "Linux/macOS"} format. Adjust paths
|
||||
according to your system. This setup is complex right now because Codex has some bugs with MCP currently.
|
||||
according to your system. This setup is complex right now because Codex has some bugs with MCP
|
||||
currently.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { STALE_TIMES } from "../../shared/queryPatterns";
|
||||
import { useSmartPolling } from "@/features/shared/hooks";
|
||||
import { STALE_TIMES } from "../../shared/config/queryPatterns";
|
||||
import { mcpApi } from "../services";
|
||||
|
||||
// Query keys factory
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { callAPIWithETag } from "../../shared/apiWithEtag";
|
||||
import { callAPIWithETag } from "../../shared/api/apiClient";
|
||||
import type { McpClient, McpServerConfig, McpServerStatus, McpSessionInfo } from "../types";
|
||||
|
||||
export const mcpApi = {
|
||||
|
||||
@ -19,7 +19,7 @@ vi.mock("../../services", () => ({
|
||||
}));
|
||||
|
||||
// Mock shared query patterns
|
||||
vi.mock("../../../shared/queryPatterns", () => ({
|
||||
vi.mock("../../../shared/config/queryPatterns", () => ({
|
||||
DISABLED_QUERY_KEY: ["disabled"] as const,
|
||||
STALE_TIMES: {
|
||||
instant: 0,
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
|
||||
import { type UseQueryResult, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { APIServiceError } from "../../shared/errors";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/queryPatterns";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/config/queryPatterns";
|
||||
import { useSmartPolling } from "../../shared/hooks";
|
||||
import { APIServiceError } from "../../shared/types/errors";
|
||||
import { progressService } from "../services";
|
||||
import type { ActiveOperationsResponse, ProgressResponse, ProgressStatus } from "../types";
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Uses ETag support for efficient polling
|
||||
*/
|
||||
|
||||
import { callAPIWithETag } from "../../shared/apiWithEtag";
|
||||
import { callAPIWithETag } from "../../shared/api/apiClient";
|
||||
import type { ActiveOperationsResponse, ProgressResponse } from "../types";
|
||||
|
||||
export const progressService = {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Activity, CheckCircle2, ListTodo } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { isOptimistic } from "../../shared/optimistic";
|
||||
import { isOptimistic } from "@/features/shared/utils/optimistic";
|
||||
import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator";
|
||||
import { cn } from "../../ui/primitives/styles";
|
||||
import type { Project } from "../types";
|
||||
|
||||
@ -13,9 +13,9 @@ import {
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import { copyToClipboard } from "../../../shared/utils/clipboard";
|
||||
import { Button } from "../../../ui/primitives";
|
||||
import type { DocumentCardProps, DocumentType } from "../types";
|
||||
import { copyToClipboard } from "../../../shared/utils/clipboard";
|
||||
|
||||
const getDocumentIcon = (type?: DocumentType) => {
|
||||
switch (type) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/queryPatterns";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/config/queryPatterns";
|
||||
import { projectService } from "../../services";
|
||||
import type { ProjectDocument } from "../types";
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSmartPolling } from "@/features/shared/hooks";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
import {
|
||||
createOptimisticEntity,
|
||||
type OptimisticEntity,
|
||||
removeDuplicateEntities,
|
||||
replaceOptimisticEntity,
|
||||
} from "@/features/shared/optimistic";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/queryPatterns";
|
||||
import { useSmartPolling } from "@/features/shared/hooks";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
} from "@/features/shared/utils/optimistic";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/config/queryPatterns";
|
||||
import { projectService } from "../services";
|
||||
import type { CreateProjectRequest, Project, UpdateProjectRequest } from "../types";
|
||||
|
||||
@ -36,9 +36,7 @@ export function useProjects() {
|
||||
|
||||
// Fetch project features
|
||||
export function useProjectFeatures(projectId: string | undefined) {
|
||||
// TODO: Phase 4 - Add explicit typing: useQuery<Awaited<ReturnType<typeof projectService.getProjectFeatures>>>
|
||||
// See PRPs/local/frontend-state-management-refactor.md Phase 4: Configure Request Deduplication
|
||||
return useQuery({
|
||||
return useQuery<Awaited<ReturnType<typeof projectService.getProjectFeatures>>>({
|
||||
queryKey: projectId ? projectKeys.features(projectId) : DISABLED_QUERY_KEY,
|
||||
queryFn: () => (projectId ? projectService.getProjectFeatures(projectId) : Promise.reject("No project ID")),
|
||||
enabled: !!projectId,
|
||||
@ -208,6 +206,8 @@ export function useDeleteProject() {
|
||||
// Don't refetch on success - trust optimistic update
|
||||
// Only remove the specific project's detail data (including nested keys)
|
||||
queryClient.removeQueries({ queryKey: projectKeys.detail(projectId), exact: false });
|
||||
// Also remove the project's feature queries
|
||||
queryClient.removeQueries({ queryKey: projectKeys.features(projectId), exact: false });
|
||||
showToast("Project deleted successfully", "success");
|
||||
},
|
||||
});
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
* Focused service for project CRUD operations only
|
||||
*/
|
||||
|
||||
import { callAPIWithETag } from "../../shared/apiWithEtag";
|
||||
import { formatZodErrors, ValidationError } from "../../shared/errors";
|
||||
import { callAPIWithETag } from "../../shared/api/apiClient";
|
||||
import { formatZodErrors, ValidationError } from "../../shared/types/errors";
|
||||
import { validateCreateProject, validateUpdateProject } from "../schemas";
|
||||
import { formatRelativeTime } from "../shared/api";
|
||||
import type { CreateProjectRequest, Project, ProjectFeatures, UpdateProjectRequest } from "../types";
|
||||
|
||||
@ -2,7 +2,7 @@ import { Tag } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { isOptimistic } from "../../../shared/optimistic";
|
||||
import { isOptimistic } from "@/features/shared/utils/optimistic";
|
||||
import { OptimisticIndicator } from "../../../ui/primitives/OptimisticIndicator";
|
||||
import { useTaskActions } from "../hooks";
|
||||
import type { Assignee, Task, TaskPriority } from "../types";
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
createOptimisticEntity,
|
||||
replaceOptimisticEntity,
|
||||
removeDuplicateEntities,
|
||||
type OptimisticEntity,
|
||||
} from "@/features/shared/optimistic";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/queryPatterns";
|
||||
removeDuplicateEntities,
|
||||
replaceOptimisticEntity,
|
||||
} from "@/features/shared/utils/optimistic";
|
||||
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/config/queryPatterns";
|
||||
import { useSmartPolling } from "../../../shared/hooks";
|
||||
import { useToast } from "../../../shared/hooks/useToast";
|
||||
import { taskService } from "../services";
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
* Focused service for task CRUD operations only
|
||||
*/
|
||||
|
||||
import { callAPIWithETag } from "../../../shared/apiWithEtag";
|
||||
import { formatZodErrors, ValidationError } from "../../../shared/errors";
|
||||
import { callAPIWithETag } from "../../../shared/api/apiClient";
|
||||
import { formatZodErrors, ValidationError } from "../../../shared/types/errors";
|
||||
|
||||
import { validateCreateTask, validateUpdateTask, validateUpdateTaskStatus } from "../schemas";
|
||||
import type { CreateTaskRequest, DatabaseTaskStatus, Task, TaskCounts, UpdateTaskRequest } from "../types";
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { callAPIWithETag } from "../../../../shared/apiWithEtag";
|
||||
import { callAPIWithETag } from "../../../../shared/api/apiClient";
|
||||
import type { CreateTaskRequest, DatabaseTaskStatus, Task, UpdateTaskRequest } from "../../types";
|
||||
import { taskService } from "../taskService";
|
||||
|
||||
// Mock the API call
|
||||
vi.mock("../../../../shared/apiWithEtag", () => ({
|
||||
vi.mock("../../../../shared/api/apiClient", () => ({
|
||||
callAPIWithETag: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@ -29,7 +29,8 @@ export function MigrationStatusCard() {
|
||||
<Database className="w-5 h-5 text-purple-400" />
|
||||
<h3 className="text-white font-semibold">Database Migrations</h3>
|
||||
</div>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="p-2 hover:bg-gray-700/50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@ -100,7 +101,8 @@ export function MigrationStatusCard() {
|
||||
? "Initial database setup is required."
|
||||
: `${data.pending_count} migration${data.pending_count > 1 ? "s" : ""} need to be applied.`}
|
||||
</p>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="px-3 py-1.5 bg-yellow-500/20 hover:bg-yellow-500/30 border border-yellow-500/50 rounded text-yellow-400 text-sm font-medium transition-colors"
|
||||
>
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { CheckCircle, Copy, Database, ExternalLink, X } from "lucide-react";
|
||||
import React from "react";
|
||||
import { copyToClipboard } from "@/features/shared/utils/clipboard";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
import { copyToClipboard } from "@/features/shared/utils/clipboard";
|
||||
import type { PendingMigration } from "../types";
|
||||
|
||||
interface PendingMigrationsModalProps {
|
||||
@ -93,7 +93,8 @@ export function PendingMigrationsModal({
|
||||
<li>Click "Refresh Status" below to verify migrations were applied</li>
|
||||
</ol>
|
||||
{migrations.length > 1 && (
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyAll}
|
||||
className="mt-3 px-3 py-1.5 bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/50 rounded text-blue-400 text-sm font-medium transition-colors"
|
||||
>
|
||||
@ -125,7 +126,8 @@ export function PendingMigrationsModal({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(migration.sql_content, index)}
|
||||
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-sm font-medium text-gray-300 flex items-center gap-2 transition-colors"
|
||||
>
|
||||
@ -141,7 +143,8 @@ export function PendingMigrationsModal({
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedIndex(expandedIndex === index ? null : index)}
|
||||
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-sm font-medium text-gray-300 transition-colors"
|
||||
>
|
||||
@ -175,13 +178,15 @@ export function PendingMigrationsModal({
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 border-t border-gray-700 flex justify-between">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-gray-300 font-medium transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={onMigrationsApplied}
|
||||
className="px-4 py-2 bg-purple-500/20 hover:bg-purple-500/30 border border-purple-500/50 rounded-lg text-purple-400 font-medium transition-colors"
|
||||
>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { STALE_TIMES } from "@/features/shared/queryPatterns";
|
||||
import { STALE_TIMES } from "@/features/shared/config/queryPatterns";
|
||||
import { useSmartPolling } from "@/features/shared/hooks/useSmartPolling";
|
||||
import { migrationService } from "../services/migrationService";
|
||||
import type { MigrationHistoryResponse, MigrationStatusResponse, PendingMigration } from "../types";
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Service for database migration tracking and management
|
||||
*/
|
||||
|
||||
import { callAPIWithETag } from "@/features/shared/apiWithEtag";
|
||||
import { callAPIWithETag } from "@/features/shared/api/apiClient";
|
||||
import type { MigrationHistoryResponse, MigrationStatusResponse, PendingMigration } from "../types";
|
||||
|
||||
export const migrationService = {
|
||||
|
||||
@ -54,7 +54,8 @@ export function UpdateBanner() {
|
||||
<span className="text-sm font-medium">View Upgrade Instructions</span>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDismissed(true)}
|
||||
className="p-2 hover:bg-gray-700/50 rounded-lg transition-colors"
|
||||
aria-label="Dismiss update banner"
|
||||
|
||||
@ -28,7 +28,8 @@ export function VersionStatusCard() {
|
||||
<Info className="w-5 h-5 text-blue-400" />
|
||||
<h3 className="text-white font-semibold">Version Information</h3>
|
||||
</div>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefreshClick}
|
||||
disabled={isLoading || clearCache.isPending}
|
||||
className="p-2 hover:bg-gray-700/50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { STALE_TIMES } from "@/features/shared/queryPatterns";
|
||||
import { STALE_TIMES } from "@/features/shared/config/queryPatterns";
|
||||
import { useSmartPolling } from "@/features/shared/hooks/useSmartPolling";
|
||||
import { versionService } from "../services/versionService";
|
||||
import type { VersionCheckResponse } from "../types";
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Service for version checking and update management
|
||||
*/
|
||||
|
||||
import { callAPIWithETag } from "@/features/shared/apiWithEtag";
|
||||
import { callAPIWithETag } from "@/features/shared/api/apiClient";
|
||||
import type { CurrentVersionResponse, VersionCheckResponse } from "../types";
|
||||
|
||||
export const versionService = {
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
* For cache control, configure TanStack Query's staleTime/gcTime instead of manual HTTP caching.
|
||||
*/
|
||||
|
||||
import { API_BASE_URL } from "../../config/api";
|
||||
import { APIServiceError } from "./errors";
|
||||
import { API_BASE_URL } from "../../../config/api";
|
||||
import { APIServiceError } from "../types/errors";
|
||||
|
||||
/**
|
||||
* Build full URL with test environment handling
|
||||
@ -48,17 +48,24 @@ export async function callAPIWithETag<T = unknown>(endpoint: string, options: Re
|
||||
// Construct the full URL
|
||||
const fullUrl = buildFullUrl(cleanEndpoint);
|
||||
|
||||
// Build headers - merge default Content-Type with provided headers
|
||||
// Build headers - only set Content-Type for requests with a body
|
||||
// NOTE: We do NOT add If-None-Match headers; the browser handles ETag revalidation automatically
|
||||
// Also note: Currently assumes headers are passed as plain objects (Record<string, string>)
|
||||
// If we ever need to support Headers instances or [string, string][] tuples,
|
||||
// we should normalize with: new Headers(options.headers), set defaults, then
|
||||
// convert back with Object.fromEntries(headers.entries())
|
||||
//
|
||||
// Currently assumes headers are passed as plain objects (Record<string, string>)
|
||||
// which works for all our current usage. The API doesn't require Accept headers
|
||||
// since it always returns JSON, and we only set Content-Type when sending data.
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...((options.headers as Record<string, string>) || {}),
|
||||
};
|
||||
|
||||
// Only set Content-Type for requests that have a body (POST, PUT, PATCH, etc.)
|
||||
// GET and DELETE requests should not have Content-Type header
|
||||
const method = options.method?.toUpperCase() || 'GET';
|
||||
const hasBody = options.body !== undefined && options.body !== null;
|
||||
if (hasBody && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
// Make the request with timeout
|
||||
// NOTE: Increased to 20s due to database performance issues with large DELETE operations
|
||||
// Root cause: Sequential scan on crawled_pages table when deleting sources with 7K+ rows
|
||||
@ -1,12 +1,12 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { callAPIWithETag } from "./apiWithEtag";
|
||||
import { APIServiceError } from "./errors";
|
||||
import { APIServiceError } from "../../types/errors";
|
||||
import { callAPIWithETag } from "../apiClient";
|
||||
|
||||
// Preserve original globals to restore after tests
|
||||
const originalAbortSignal = global.AbortSignal as any;
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
describe("apiWithEtag", () => {
|
||||
describe("apiClient (callAPIWithETag)", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// Reset fetch to undefined to ensure clean state
|
||||
@ -258,16 +258,16 @@ describe("apiWithEtag", () => {
|
||||
});
|
||||
|
||||
it("should work seamlessly with TanStack Query's caching strategy", async () => {
|
||||
// This test documents how our simplified approach works with TanStack Query:
|
||||
// This test documents how the API client integrates with TanStack Query:
|
||||
// 1. TanStack Query calls our function when data is stale
|
||||
// 2. We make a simple fetch request
|
||||
// 2. We make a fetch request
|
||||
// 3. Browser adds If-None-Match if it has cached data
|
||||
// 4. Server returns 200 (new data) or 304 (not modified)
|
||||
// 5. Browser returns data to us (either new or cached)
|
||||
// 6. We return data to TanStack Query
|
||||
// 7. TanStack Query updates its cache
|
||||
|
||||
const mockData = { workflow: "simplified" };
|
||||
const mockData = { workflow: "standard" };
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
@ -285,8 +285,8 @@ describe("apiWithEtag", () => {
|
||||
});
|
||||
|
||||
it("should allow browser to optimize bandwidth automatically", async () => {
|
||||
// This test verifies that even though we removed explicit ETag handling,
|
||||
// bandwidth optimization still works through browser's HTTP cache
|
||||
// This test verifies that ETag negotiation is handled by the browser
|
||||
// and bandwidth optimization works through the browser's HTTP cache
|
||||
|
||||
const mockData = { size: "large", benefit: "bandwidth saved" };
|
||||
const mockResponse = {
|
||||
@ -310,7 +310,7 @@ describe("apiWithEtag", () => {
|
||||
});
|
||||
|
||||
it("should handle server errors regardless of caching", async () => {
|
||||
// Verify error handling still works in simplified version
|
||||
// Verify error handling works with standard fetch approach
|
||||
const errorResponse = {
|
||||
ok: false,
|
||||
status: 500,
|
||||
@ -1,3 +1,3 @@
|
||||
export * from "./useSmartPolling";
|
||||
export * from "./useThemeAware";
|
||||
export * from "./useToast";
|
||||
export * from "./useToast";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AlertCircle, CheckCircle, Info, XCircle } from "lucide-react";
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { createOptimisticId } from "../optimistic";
|
||||
import { createOptimisticId } from "../utils/optimistic";
|
||||
|
||||
// Toast types
|
||||
interface Toast {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createOptimisticId,
|
||||
createOptimisticEntity,
|
||||
isOptimistic,
|
||||
replaceOptimisticEntity,
|
||||
removeDuplicateEntities,
|
||||
cleanOptimisticMetadata,
|
||||
} from "./optimistic";
|
||||
createOptimisticEntity,
|
||||
createOptimisticId,
|
||||
isOptimistic,
|
||||
removeDuplicateEntities,
|
||||
replaceOptimisticEntity,
|
||||
} from "../optimistic";
|
||||
|
||||
describe("Optimistic Update Utilities", () => {
|
||||
describe("createOptimisticId", () => {
|
||||
@ -1,7 +1,7 @@
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render as rtlRender } from "@testing-library/react";
|
||||
import type React from "react";
|
||||
import { createTestQueryClient } from "../shared/queryClient";
|
||||
import { createTestQueryClient } from "../shared/config/queryClient";
|
||||
import { ToastProvider } from "../ui/components/ToastProvider";
|
||||
import { TooltipProvider } from "../ui/primitives/tooltip";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user