refactor: Reorganize frontend by moving UI components, hooks, and utilities to new components, logic, features, and lib directories
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { httpClient, setAuthToken, clearAuthToken } from '../lib/apiClient';
|
||||
import { createSafeContext } from '../utils/createSafeContext';
|
||||
|
||||
type HttpClient = typeof httpClient;
|
||||
|
||||
interface ApiContextValue {
|
||||
client: HttpClient;
|
||||
setAuthToken: (token: string) => void;
|
||||
clearAuthToken: () => void;
|
||||
}
|
||||
|
||||
const [ApiContext, useApi] = createSafeContext<ApiContextValue>('Api');
|
||||
|
||||
export const ApiProvider: React.FC<PropsWithChildren<{ initialToken?: string | null }>> = ({
|
||||
initialToken = null,
|
||||
children,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (initialToken) {
|
||||
setAuthToken(initialToken);
|
||||
}
|
||||
}, [initialToken]);
|
||||
|
||||
const value = useMemo<ApiContextValue>(
|
||||
() => ({
|
||||
client: httpClient,
|
||||
setAuthToken,
|
||||
clearAuthToken,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;
|
||||
};
|
||||
|
||||
export { useApi };
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
DocumentsFilterProvider,
|
||||
} from '../documents/context/DocumentsFilterContext';
|
||||
import { PreviewProvider, usePreviewContext } from '../preview/PreviewContext';
|
||||
import { DocumentOpenProvider } from '../contexts/DocumentOpenContext';
|
||||
import { DocumentOpenProvider } from '../lib/context/DocumentOpenContext';
|
||||
import { useWorkspaceSurface } from './useWorkspaceSurface';
|
||||
import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader';
|
||||
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
serializeAuthenticationCredential,
|
||||
serializeRegistrationCredential,
|
||||
} from '../utils/webauthn';
|
||||
import { useAppDispatch, useAppState } from './appState';
|
||||
import { useAppDispatch, useAppState } from '../lib/store/appState';
|
||||
import {
|
||||
finishPasskeyLogin,
|
||||
finishSignup,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
selectTenant,
|
||||
startPasskeyLogin,
|
||||
startSignup,
|
||||
} from '../lib/apiClient';
|
||||
} from '../lib/api/apiClient';
|
||||
|
||||
type StatusVariant = 'info' | 'success' | 'error';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import SettingsModal from '../settings/SettingsModal';
|
||||
import { useAppShell } from '../appShellContext';
|
||||
import { useAppShell } from '../lib/context/AppShellContext';
|
||||
import useApiTokens from '../settings/useApiTokens';
|
||||
import useCapabilitySets from '../settings/useCapabilitySets';
|
||||
import useCapabilities from '../settings/useCapabilities';
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
WarningIcon,
|
||||
BottombarCollapseIcon,
|
||||
BottombarExpandIcon,
|
||||
} from '../ui/icons';
|
||||
import PanelHeader from '../ui/PanelHeader';
|
||||
} from '../components/icons';
|
||||
import PanelHeader from '../components/PanelHeader';
|
||||
|
||||
type UploadStatus = 'pending' | 'uploading' | 'success' | 'duplicate' | 'error' | (string & {});
|
||||
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import React, { useEffect, useMemo, useReducer } from 'react';
|
||||
import { createSafeContext } from '../utils/createSafeContext';
|
||||
import { clearAuthToken, setAuthToken, setAuthRefreshHandlers } from '../lib/apiClient';
|
||||
import { ApiProvider } from './ApiContext';
|
||||
import { listTenants } from '../lib/apiClient';
|
||||
import { STORED_TOKEN_KEY } from '../constants/app';
|
||||
|
||||
type Tenant = Record<string, unknown> | null;
|
||||
|
||||
interface TenantSelection {
|
||||
selectionToken: string;
|
||||
tenants: Tenant[];
|
||||
}
|
||||
|
||||
type AppStatus =
|
||||
| 'logged-out'
|
||||
| 'authenticating'
|
||||
| 'authenticated'
|
||||
| 'selecting-tenant'
|
||||
| 'bootstrapping'
|
||||
| 'ready';
|
||||
|
||||
interface AppState {
|
||||
status: AppStatus;
|
||||
token: string;
|
||||
error: string | null;
|
||||
isRefreshing: boolean;
|
||||
tenantSelection: TenantSelection | null;
|
||||
tenant: Tenant;
|
||||
tenants: Tenant[];
|
||||
}
|
||||
|
||||
type AppAction =
|
||||
| { type: 'LOGIN_REQUEST' }
|
||||
| { type: 'LOGIN_SUCCESS'; token: string; tenant?: Tenant }
|
||||
| { type: 'LOGIN_FAILURE'; error?: string | null }
|
||||
| { type: 'TENANT_SELECTION_REQUIRED'; selectionToken: string; tenants: Tenant[] }
|
||||
| { type: 'CLEAR_TENANT_SELECTION' }
|
||||
| { type: 'LOGOUT_SUCCESS' }
|
||||
| { type: 'BOOTSTRAP_START' }
|
||||
| { type: 'BOOTSTRAP_SUCCESS' }
|
||||
| { type: 'BOOTSTRAP_FAILURE'; error?: string | null }
|
||||
| { type: 'TOKEN_REFRESH_START' }
|
||||
| { type: 'TOKEN_REFRESH_SUCCESS'; token: string; tenant?: Tenant }
|
||||
| { type: 'TOKEN_REFRESH_FAILURE'; error?: string | null }
|
||||
| { type: 'LOGOUT' }
|
||||
| { type: 'RESET_ERROR' }
|
||||
| { type: 'SET_TENANTS'; tenants: Tenant[] };
|
||||
|
||||
const storage = window.sessionStorage;
|
||||
|
||||
const storedToken = storage?.getItem(STORED_TOKEN_KEY) ?? '';
|
||||
let STORED_TENANT: Tenant = null;
|
||||
|
||||
if (storage) {
|
||||
try {
|
||||
const rawTenant = storage.getItem('papercrate_tenant');
|
||||
if (rawTenant) {
|
||||
STORED_TENANT = JSON.parse(rawTenant);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[app] Failed to parse stored tenant metadata', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (storedToken) {
|
||||
setAuthToken(storedToken);
|
||||
}
|
||||
|
||||
const initialAppState: AppState = {
|
||||
status: storedToken ? 'authenticated' : 'logged-out',
|
||||
token: storedToken,
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: STORED_TENANT,
|
||||
tenants: [],
|
||||
};
|
||||
|
||||
const [AppStateContext, useAppState] = createSafeContext<AppState>('AppState');
|
||||
const [AppDispatchContext, useAppDispatch] = createSafeContext<React.Dispatch<AppAction>>('AppDispatch');
|
||||
|
||||
const appStateReducer = (state: AppState, action: AppAction): AppState => {
|
||||
switch (action.type) {
|
||||
case 'LOGIN_REQUEST':
|
||||
return {
|
||||
...state,
|
||||
status: 'authenticating',
|
||||
error: null,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'LOGIN_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
status: 'authenticated',
|
||||
token: action.token,
|
||||
error: null,
|
||||
tenantSelection: null,
|
||||
tenant: action.tenant ?? null,
|
||||
tenants: state.tenants,
|
||||
};
|
||||
case 'LOGIN_FAILURE':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: action.error ?? null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'TENANT_SELECTION_REQUIRED':
|
||||
return {
|
||||
status: 'selecting-tenant',
|
||||
token: '',
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: {
|
||||
selectionToken: action.selectionToken,
|
||||
tenants: action.tenants,
|
||||
},
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'CLEAR_TENANT_SELECTION':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'LOGOUT_SUCCESS':
|
||||
case 'LOGOUT':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'BOOTSTRAP_START':
|
||||
return { ...state, status: 'bootstrapping', error: null };
|
||||
case 'BOOTSTRAP_SUCCESS':
|
||||
return { ...state, status: 'ready', error: null };
|
||||
case 'BOOTSTRAP_FAILURE':
|
||||
return { ...state, status: 'authenticated', error: action.error ?? null };
|
||||
case 'TOKEN_REFRESH_START':
|
||||
return { ...state, isRefreshing: true, error: null };
|
||||
case 'TOKEN_REFRESH_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
token: action.token,
|
||||
isRefreshing: false,
|
||||
status: state.status === 'logged-out' ? 'authenticated' : state.status,
|
||||
tenantSelection: null,
|
||||
tenant: action.tenant ?? state.tenant ?? null,
|
||||
tenants: state.tenants,
|
||||
};
|
||||
case 'TOKEN_REFRESH_FAILURE':
|
||||
return {
|
||||
status: 'logged-out',
|
||||
token: '',
|
||||
error: action.error ?? null,
|
||||
isRefreshing: false,
|
||||
tenantSelection: null,
|
||||
tenant: null,
|
||||
tenants: [],
|
||||
};
|
||||
case 'RESET_ERROR':
|
||||
return { ...state, error: null };
|
||||
case 'SET_TENANTS':
|
||||
return {
|
||||
...state,
|
||||
tenants: Array.isArray(action.tenants) ? action.tenants : [],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(appStateReducer, initialAppState);
|
||||
|
||||
useEffect(() => {
|
||||
const token = state.token ?? '';
|
||||
if (token) {
|
||||
setAuthToken(token);
|
||||
storage?.setItem('papercrate_token', token);
|
||||
} else {
|
||||
clearAuthToken();
|
||||
storage?.removeItem('papercrate_token');
|
||||
}
|
||||
}, [state.token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.tenant) {
|
||||
try {
|
||||
storage?.setItem('papercrate_tenant', JSON.stringify(state.tenant));
|
||||
} catch (error) {
|
||||
console.warn('[app] Failed to persist tenant info', error);
|
||||
}
|
||||
} else {
|
||||
storage?.removeItem('papercrate_tenant');
|
||||
}
|
||||
}, [state.tenant]);
|
||||
|
||||
useEffect(() => {
|
||||
setAuthRefreshHandlers({
|
||||
onRefreshSuccess: (token, payload) => {
|
||||
dispatch({ type: 'TOKEN_REFRESH_SUCCESS', token, tenant: payload?.tenant ?? null });
|
||||
},
|
||||
onRefreshFailure: (error) => {
|
||||
dispatch({ type: 'TOKEN_REFRESH_FAILURE', error: (error as Error)?.message || null });
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
setAuthRefreshHandlers({});
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
let abort = false;
|
||||
|
||||
const loadTenants = async () => {
|
||||
if (state.status !== 'authenticated' || !state.token) {
|
||||
dispatch({ type: 'SET_TENANTS', tenants: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!abort) {
|
||||
const tenants = await listTenants();
|
||||
dispatch({
|
||||
type: 'SET_TENANTS',
|
||||
tenants,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (!abort) {
|
||||
console.warn('Failed to load tenant list', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadTenants();
|
||||
|
||||
return () => {
|
||||
abort = true;
|
||||
};
|
||||
}, [state.status, state.token, dispatch]);
|
||||
|
||||
const stateValue = useMemo(() => state, [state]);
|
||||
|
||||
return (
|
||||
<ApiProvider initialToken={state.token}>
|
||||
<AppStateContext.Provider value={stateValue}>
|
||||
<AppDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</AppDispatchContext.Provider>
|
||||
</AppStateContext.Provider>
|
||||
</ApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { AppStateProvider, useAppState, useAppDispatch };
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { TAG_FILTER_UNTAGGED } from './workspaceUtils';
|
||||
import { listDocuments } from '../lib/apiClient';
|
||||
import { listDocuments } from '../lib/api/apiClient';
|
||||
import type { Identifier } from '../types/identifiers';
|
||||
|
||||
import type { Document } from '../types/documents';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useAppShell } from '../appShellContext';
|
||||
import { useAppShell } from '../lib/context/AppShellContext';
|
||||
import type { DocumentsFilterValue } from '../documents/context/DocumentsFilterContext';
|
||||
import type Sidebar from '../sidebar/Sidebar';
|
||||
import type { UseWorkspaceSurfaceArgs } from './useWorkspaceSurface';
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import TagsPanel from '../tags/TagsPanel';
|
||||
import CorrespondentsPanel, { CorrespondentsPanelProps } from '../correspondents/CorrespondentsPanel';
|
||||
import PanelHeader from '../ui/PanelHeader';
|
||||
import { CloseIcon } from '../ui/icons';
|
||||
import PanelHeader from '../components/PanelHeader';
|
||||
import { CloseIcon } from '../components/icons';
|
||||
import { CORRESPONDENTS_MODAL, TAGS_MODAL } from '../constants/app';
|
||||
|
||||
interface TagRecord {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { SidebarExpandIcon } from '../ui/icons';
|
||||
import { SidebarExpandIcon } from '../components/icons';
|
||||
import DocumentsPanel from '../documents/panel/DocumentsPanel';
|
||||
import DocumentViewerPanel from '../preview/DocumentViewerPanel';
|
||||
import { usePanelManager } from './PanelManagerContext';
|
||||
|
||||
Reference in New Issue
Block a user