fix(mobile): iOS share sheet, empty files tab, and stuck upload status
- app.json: add CFBundleDocumentTypes to iOS infoPlist so the app appears in the iOS Share Sheet; add ACTION_SEND/SEND_MULTIPLE intentFilters for Android share intent support - src/context/ShareContext.tsx (new): React context that queues files received from the share sheet and delivers them to UploadScreen - app/_layout.tsx: wrap in ShareProvider; add Linking handler (makeUrlHandler factory + getInitialURL cold-start + addEventListener warm-start) to capture file:// and content:// URLs - src/services/api.ts: fix FileRecord interface (original_filename, nested ProcessingStatus, mime_type); fix UploadResponse interface; fix listFiles() (per_page param, unwrap data.files); add getFileStatus(fileId) for single-file status polling - src/screens/FilesScreen.tsx: use file.original_filename and file.processing_status.status; fix statusEmoji to use actual backend status values (completed/pending/duplicate) - src/screens/UploadScreen.tsx: consume ShareContext for auto-upload of shared files; add 5-second polling loop (search by filename → file_id → getFileStatus) to show real-time server processing status; uploadFile wrapped in useCallback; proper effect dependency arrays Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+24
-8
@@ -119,7 +119,8 @@ mobile/
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
├── context/
|
||||
│ └── AuthContext.tsx # Authentication state management
|
||||
│ ├── AuthContext.tsx # Authentication state management
|
||||
│ └── ShareContext.tsx # Shared-file queue (iOS Share Sheet / Android Intent)
|
||||
├── hooks/
|
||||
│ └── usePushNotifications.ts # Push notification registration
|
||||
├── screens/
|
||||
@@ -132,16 +133,30 @@ mobile/
|
||||
└── api.ts # DocuElevate API client
|
||||
```
|
||||
|
||||
## Share Extension (iOS)
|
||||
## Share Sheet (iOS) / Share Intent (Android)
|
||||
|
||||
The app registers the `docuelevate://` URL scheme and the `com.docuelevate.app` bundle identifier. To enable the share sheet:
|
||||
The app registers itself as a share target so any file can be sent directly to DocuElevate from another app.
|
||||
|
||||
1. Ensure the app is installed on the device
|
||||
2. Open any file in Files, Mail, Safari, etc.
|
||||
3. Tap the share icon → find **DocuElevate** in the share sheet
|
||||
4. The file is uploaded immediately
|
||||
### iOS – how it works
|
||||
|
||||
Android uses a similar intent filter configured in `app.json`.
|
||||
`app.json` declares `CFBundleDocumentTypes` in the iOS `infoPlist` section. This tells iOS which file types the app can receive, causing it to appear in the share sheet when the user shares a matching file. When the user taps **DocuElevate** in the share sheet, iOS passes the file path to the app via `application:openURL:options:`, which React Native forwards as a `file://` URL through the `Linking` module.
|
||||
|
||||
The root layout (`app/_layout.tsx`) listens for incoming `file://` URLs via `Linking.addEventListener` (warm start) and `Linking.getInitialURL()` (cold start). Incoming files are stored in `ShareContext` and automatically uploaded by `UploadScreen`.
|
||||
|
||||
**Supported iOS file types:** PDF, images (JPEG / PNG / GIF / BMP / TIFF / WebP), plain text, Word (`.docx`, `.doc`), Excel (`.xlsx`, `.xls`), PowerPoint (`.pptx`, `.ppt`), and any other file (`public.data`).
|
||||
|
||||
To use the share sheet:
|
||||
|
||||
1. Ensure the app is installed on the device.
|
||||
2. Open any supported file in Files, Mail, Safari, etc.
|
||||
3. Tap the **Share** button → find **DocuElevate** in the share sheet.
|
||||
4. The file is uploaded immediately.
|
||||
|
||||
> **Note:** `CFBundleDocumentTypes` with `LSHandlerRank: Alternate` means DocuElevate appears in the share sheet as an option but does **not** become the default app for any file type.
|
||||
|
||||
### Android – how it works
|
||||
|
||||
`app.json` declares `intentFilters` for `ACTION_SEND` and `ACTION_SEND_MULTIPLE` with `mimeType: "*/*"`. When a user shares a file from another app and selects DocuElevate, Android delivers the content URI through the share intent, which is captured via `Linking.getInitialURL()` and processed the same way as on iOS.
|
||||
|
||||
## Backend API
|
||||
|
||||
@@ -156,6 +171,7 @@ The mobile app uses the following backend endpoints:
|
||||
| `GET` | `/api/mobile/whoami` | Get current user profile |
|
||||
| `POST` | `/api/ui-upload` | Upload file for processing |
|
||||
| `GET` | `/api/files` | List processed documents |
|
||||
| `GET` | `/api/files/{id}` | Get processing status of a single file |
|
||||
|
||||
Authentication uses `Authorization: Bearer <api_token>` on all requests.
|
||||
|
||||
|
||||
+33
-1
@@ -21,7 +21,27 @@
|
||||
"NSPhotoLibraryUsageDescription": "DocuElevate accesses your photo library to select documents for upload.",
|
||||
"NSPhotoLibraryAddUsageDescription": "DocuElevate saves scanned documents to your photo library.",
|
||||
"UIBackgroundModes": ["fetch", "remote-notification"],
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
"ITSAppUsesNonExemptEncryption": false,
|
||||
"CFBundleDocumentTypes": [
|
||||
{
|
||||
"CFBundleTypeName": "All Documents",
|
||||
"CFBundleTypeRole": "Viewer",
|
||||
"LSHandlerRank": "Alternate",
|
||||
"LSItemContentTypes": [
|
||||
"public.content",
|
||||
"public.data",
|
||||
"public.image",
|
||||
"com.adobe.pdf",
|
||||
"public.plain-text",
|
||||
"org.openxmlformats.wordprocessingml.document",
|
||||
"org.openxmlformats.spreadsheetml.sheet",
|
||||
"org.openxmlformats.presentationml.presentation",
|
||||
"com.microsoft.word.doc",
|
||||
"com.microsoft.excel.xls",
|
||||
"com.microsoft.powerpoint.ppt"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"buildNumber": "3"
|
||||
},
|
||||
@@ -38,6 +58,18 @@
|
||||
"RECEIVE_BOOT_COMPLETED",
|
||||
"VIBRATE"
|
||||
],
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "SEND",
|
||||
"data": [{ "mimeType": "*/*" }],
|
||||
"category": ["DEFAULT"]
|
||||
},
|
||||
{
|
||||
"action": "SEND_MULTIPLE",
|
||||
"data": [{ "mimeType": "*/*" }],
|
||||
"category": ["DEFAULT"]
|
||||
}
|
||||
],
|
||||
"versionCode": 1
|
||||
},
|
||||
"web": {
|
||||
|
||||
+57
-3
@@ -4,13 +4,48 @@
|
||||
* Wraps the entire app in AuthProvider and SafeAreaProvider, then uses the
|
||||
* AuthGuard component to redirect between the unauthenticated (auth) route
|
||||
* group and the authenticated (tabs) route group based on session state.
|
||||
*
|
||||
* ShareProvider + Linking listener: when iOS opens the app via the share
|
||||
* sheet (CFBundleDocumentTypes) or Android via a SEND intent, the incoming
|
||||
* file:// / content:// URL is captured and forwarded to UploadScreen via
|
||||
* ShareContext.
|
||||
*/
|
||||
|
||||
import * as Linking from "expo-linking";
|
||||
import { Stack, useRouter, useSegments } from "expo-router";
|
||||
import React, { useEffect } from "react";
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { AuthProvider, useAuth } from "../src/context/AuthContext";
|
||||
import { ShareProvider, useShare } from "../src/context/ShareContext";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Extract a display filename from a file:// or content:// URI. */
|
||||
function filenameFromUri(uri: string): string {
|
||||
try {
|
||||
const decoded = decodeURIComponent(uri);
|
||||
// Take the last path segment and strip any query string
|
||||
const last = decoded.split("/").pop() ?? "shared_file";
|
||||
return last.split("?")[0] || "shared_file";
|
||||
} catch {
|
||||
return "shared_file";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Linking URL handler that forwards incoming file:// / content://
|
||||
* URLs to ShareContext. Extracted as a module-level factory so the handler
|
||||
* itself is created once and can be easily unit-tested without a React context.
|
||||
*/
|
||||
function makeUrlHandler(addPendingFile: (f: { uri: string; filename: string }) => void) {
|
||||
return ({ url }: { url: string }) => {
|
||||
if (!url.startsWith("file://") && !url.startsWith("content://")) return;
|
||||
addPendingFile({ uri: url, filename: filenameFromUri(url) });
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth guard – redirects to the correct route group after auth state resolves
|
||||
@@ -18,9 +53,26 @@ import { AuthProvider, useAuth } from "../src/context/AuthContext";
|
||||
|
||||
function AuthGuard() {
|
||||
const { isLoading, isAuthenticated } = useAuth();
|
||||
const { addPendingFile } = useShare();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
// Listen for files shared from other apps (iOS Share Sheet / Android Intent).
|
||||
// Both cold-start (app was not running) and warm-start (app in background)
|
||||
// cases are handled.
|
||||
useEffect(() => {
|
||||
const handleIncomingUrl = makeUrlHandler(addPendingFile);
|
||||
|
||||
// Cold start – app launched directly by a share action
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) handleIncomingUrl({ url });
|
||||
});
|
||||
|
||||
// Warm start – app was already running when the share action occurred
|
||||
const subscription = Linking.addEventListener("url", handleIncomingUrl);
|
||||
return () => subscription.remove();
|
||||
}, [addPendingFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
@@ -59,9 +111,11 @@ function AuthGuard() {
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<AuthProvider>
|
||||
<AuthGuard />
|
||||
</AuthProvider>
|
||||
<ShareProvider>
|
||||
<AuthProvider>
|
||||
<AuthGuard />
|
||||
</AuthProvider>
|
||||
</ShareProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* ShareContext – propagates files received from the iOS Share Sheet or the
|
||||
* Android Share Intent to the UploadScreen so they can be uploaded
|
||||
* automatically.
|
||||
*
|
||||
* The root layout listens for incoming file:// / content:// URLs via
|
||||
* expo-linking and calls addPendingFile(). UploadScreen consumes the context,
|
||||
* uploads each pending file, then calls clearPendingFiles().
|
||||
*/
|
||||
|
||||
import React, { createContext, useCallback, useContext, useState } from "react";
|
||||
|
||||
export interface SharedFile {
|
||||
uri: string;
|
||||
filename: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
interface ShareContextValue {
|
||||
pendingFiles: SharedFile[];
|
||||
addPendingFile: (file: SharedFile) => void;
|
||||
clearPendingFiles: () => void;
|
||||
}
|
||||
|
||||
const ShareContext = createContext<ShareContextValue>({
|
||||
pendingFiles: [],
|
||||
addPendingFile: () => {},
|
||||
clearPendingFiles: () => {},
|
||||
});
|
||||
|
||||
export function ShareProvider({ children }: { children: React.ReactNode }) {
|
||||
const [pendingFiles, setPendingFiles] = useState<SharedFile[]>([]);
|
||||
|
||||
const addPendingFile = useCallback((file: SharedFile) => {
|
||||
setPendingFiles((prev) => [...prev, file]);
|
||||
}, []);
|
||||
|
||||
const clearPendingFiles = useCallback(() => {
|
||||
setPendingFiles([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ShareContext.Provider value={{ pendingFiles, addPendingFile, clearPendingFiles }}>
|
||||
{children}
|
||||
</ShareContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useShare(): ShareContextValue {
|
||||
return useContext(ShareContext);
|
||||
}
|
||||
@@ -36,11 +36,11 @@ function formatDate(iso: string): string {
|
||||
|
||||
function statusEmoji(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
processed: "✅",
|
||||
completed: "✅",
|
||||
processing: "⚙️",
|
||||
queued: "⏳",
|
||||
pending: "⏳",
|
||||
failed: "❌",
|
||||
uploaded: "⬆️",
|
||||
duplicate: "🔁",
|
||||
};
|
||||
return map[status?.toLowerCase()] ?? "📄";
|
||||
}
|
||||
@@ -143,18 +143,19 @@ export default function FilesScreen() {
|
||||
}
|
||||
|
||||
function FileRow({ file }: { file: FileRecord }) {
|
||||
const status = file.processing_status?.status ?? "pending";
|
||||
return (
|
||||
<View style={rowStyles.row}>
|
||||
<Text style={rowStyles.icon}>{statusEmoji(file.status)}</Text>
|
||||
<Text style={rowStyles.icon}>{statusEmoji(status)}</Text>
|
||||
<View style={rowStyles.info}>
|
||||
<Text style={rowStyles.filename} numberOfLines={1}>
|
||||
{file.filename}
|
||||
{file.original_filename}
|
||||
</Text>
|
||||
<Text style={rowStyles.meta}>
|
||||
{formatDate(file.created_at)} · {formatBytes(file.file_size)}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={rowStyles.status}>{file.status}</Text>
|
||||
<Text style={rowStyles.status}>{status}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
* 1. Take a photo of a document with the device camera.
|
||||
* 2. Pick an existing file (PDF, image, Office document) from the Files app.
|
||||
* 3. Receive files shared from other apps via the iOS Share Sheet / Android
|
||||
* Share Intent (handled by the expo-sharing + deep-link integration).
|
||||
* Share Intent (handled via ShareContext populated by the root layout).
|
||||
*
|
||||
* After a successful upload the screen polls the backend every 5 seconds to
|
||||
* track the real-time processing status of each uploaded file.
|
||||
*/
|
||||
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
@@ -21,41 +24,119 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useShare } from "../context/ShareContext";
|
||||
import api from "../services/api";
|
||||
|
||||
/** Statuses that indicate processing has finished (no further polling needed). */
|
||||
const TERMINAL_STATUSES = new Set(["completed", "failed", "duplicate"]);
|
||||
|
||||
interface UploadItem {
|
||||
id: string;
|
||||
filename: string;
|
||||
status: "pending" | "uploading" | "done" | "error";
|
||||
error?: string;
|
||||
taskId?: string;
|
||||
/** Sanitised filename returned by the server – used to search for the record. */
|
||||
originalFilename?: string;
|
||||
/** Database ID of the FileRecord once it has been created by the worker. */
|
||||
fileId?: number;
|
||||
/** Actual server-side processing status (e.g. "pending", "processing", "completed"). */
|
||||
serverStatus?: string;
|
||||
}
|
||||
|
||||
export default function UploadScreen() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { pendingFiles, clearPendingFiles } = useShare();
|
||||
const [uploads, setUploads] = useState<UploadItem[]>([]);
|
||||
|
||||
function updateItem(id: string, patch: Partial<UploadItem>) {
|
||||
setUploads((prev) =>
|
||||
prev.map((item) => (item.id === id ? { ...item, ...patch } : item))
|
||||
);
|
||||
}
|
||||
// Keep a ref in sync so the polling interval can read current state without
|
||||
// capturing a stale closure.
|
||||
const uploadsRef = useRef<UploadItem[]>([]);
|
||||
useEffect(() => {
|
||||
uploadsRef.current = uploads;
|
||||
}, [uploads]);
|
||||
|
||||
async function uploadFile(uri: string, filename: string, mimeType?: string) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core helpers (declared before the effects that depend on them)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const uploadFile = useCallback(async (uri: string, filename: string, mimeType?: string) => {
|
||||
const id = `${Date.now()}-${filename}`;
|
||||
setUploads((prev) => [
|
||||
{ id, filename, status: "uploading" },
|
||||
...prev,
|
||||
]);
|
||||
setUploads((prev) => [{ id, filename, status: "uploading" }, ...prev]);
|
||||
|
||||
try {
|
||||
const resp = await api.uploadFile(uri, filename, mimeType);
|
||||
updateItem(id, { status: "done", taskId: resp.task_id });
|
||||
setUploads((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id
|
||||
? { ...item, status: "done", taskId: resp.task_id, originalFilename: resp.original_filename }
|
||||
: item
|
||||
)
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : "Upload failed";
|
||||
updateItem(id, { status: "error", error: msg });
|
||||
setUploads((prev) =>
|
||||
prev.map((item) => (item.id === id ? { ...item, status: "error", error: msg } : item))
|
||||
);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Polling – check server-side processing status every 5 seconds
|
||||
// ---------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const poll = async () => {
|
||||
const current = uploadsRef.current;
|
||||
for (const item of current) {
|
||||
// Only poll items that were successfully uploaded and haven't reached a
|
||||
// terminal status yet.
|
||||
if (item.status !== "done") continue;
|
||||
if (item.serverStatus && TERMINAL_STATUSES.has(item.serverStatus)) continue;
|
||||
|
||||
try {
|
||||
if (item.fileId !== undefined) {
|
||||
// We already know the file ID – just refresh its status.
|
||||
const ps = await api.getFileStatus(item.fileId);
|
||||
setUploads((prev) =>
|
||||
prev.map((u) => (u.id === item.id ? { ...u, serverStatus: ps.status } : u))
|
||||
);
|
||||
} else if (item.originalFilename) {
|
||||
// Search for the file by name; it may not exist yet if the worker
|
||||
// hasn't started.
|
||||
const files = await api.listFiles(1, 5, item.originalFilename);
|
||||
const found = files.find((f) => f.original_filename === item.originalFilename);
|
||||
if (found) {
|
||||
setUploads((prev) =>
|
||||
prev.map((u) =>
|
||||
u.id === item.id
|
||||
? { ...u, fileId: found.id, serverStatus: found.processing_status.status }
|
||||
: u
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Network errors are transient – silently retry on the next tick.
|
||||
console.debug("[StatusPoll] failed:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(poll, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []); // Intentionally empty – poll() reads state via uploadsRef
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handle files received from the iOS Share Sheet / Android Share Intent
|
||||
// ---------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (pendingFiles.length === 0 || !isAuthenticated) return;
|
||||
const files = [...pendingFiles];
|
||||
clearPendingFiles();
|
||||
files.forEach((file) => {
|
||||
void uploadFile(file.uri, file.filename, file.mimeType);
|
||||
});
|
||||
}, [pendingFiles, isAuthenticated, clearPendingFiles, uploadFile]);
|
||||
|
||||
async function handleCamera() {
|
||||
const { status } = await ImagePicker.requestCameraPermissionsAsync();
|
||||
@@ -154,16 +235,28 @@ export default function UploadScreen() {
|
||||
}
|
||||
|
||||
function UploadRow({ item }: { item: UploadItem }) {
|
||||
const icons: Record<UploadItem["status"], string> = {
|
||||
const uploadIcons: Record<UploadItem["status"], string> = {
|
||||
pending: "⏳",
|
||||
uploading: "⬆️",
|
||||
done: "✅",
|
||||
error: "❌",
|
||||
};
|
||||
|
||||
/** Human-readable label for the server-side processing status. */
|
||||
function serverStatusLabel(s: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
pending: "Queued for processing…",
|
||||
processing: "Processing…",
|
||||
completed: "Processed ✓",
|
||||
failed: "Processing failed",
|
||||
duplicate: "Duplicate – already processed",
|
||||
};
|
||||
return labels[s] ?? s;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={rowStyles.row}>
|
||||
<Text style={rowStyles.icon}>{icons[item.status]}</Text>
|
||||
<Text style={rowStyles.icon}>{uploadIcons[item.status]}</Text>
|
||||
<View style={rowStyles.info}>
|
||||
<Text style={rowStyles.filename} numberOfLines={1}>
|
||||
{item.filename}
|
||||
@@ -171,8 +264,21 @@ function UploadRow({ item }: { item: UploadItem }) {
|
||||
{item.status === "uploading" && (
|
||||
<ActivityIndicator size="small" color="#1e40af" />
|
||||
)}
|
||||
{item.status === "done" && (
|
||||
<Text style={rowStyles.statusDone}>Queued for processing</Text>
|
||||
{item.status === "done" && !item.serverStatus && (
|
||||
<Text style={rowStyles.statusQueued}>Queued for processing…</Text>
|
||||
)}
|
||||
{item.status === "done" && item.serverStatus && (
|
||||
<Text
|
||||
style={
|
||||
item.serverStatus === "completed"
|
||||
? rowStyles.statusDone
|
||||
: item.serverStatus === "failed"
|
||||
? rowStyles.statusError
|
||||
: rowStyles.statusQueued
|
||||
}
|
||||
>
|
||||
{serverStatusLabel(item.serverStatus)}
|
||||
</Text>
|
||||
)}
|
||||
{item.status === "error" && (
|
||||
<Text style={rowStyles.statusError}>{item.error}</Text>
|
||||
@@ -254,5 +360,6 @@ const rowStyles = StyleSheet.create({
|
||||
marginBottom: 4,
|
||||
},
|
||||
statusDone: { fontSize: 12, color: "#059669" },
|
||||
statusQueued: { fontSize: 12, color: "#6b7280" },
|
||||
statusError: { fontSize: 12, color: "#dc2626" },
|
||||
});
|
||||
|
||||
+26
-11
@@ -41,21 +41,27 @@ export interface DeviceRegistration {
|
||||
platform: "ios" | "android" | "web";
|
||||
}
|
||||
|
||||
export interface ProcessingStatus {
|
||||
status: string;
|
||||
last_step: string | null;
|
||||
has_errors: boolean;
|
||||
total_steps: number;
|
||||
}
|
||||
|
||||
export interface FileRecord {
|
||||
id: number;
|
||||
filename: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
original_filename: string;
|
||||
file_size: number | null;
|
||||
content_type: string | null;
|
||||
owner_id: string | null;
|
||||
mime_type: string | null;
|
||||
created_at: string;
|
||||
processing_status: ProcessingStatus;
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
task_id: string;
|
||||
status: string;
|
||||
message: string;
|
||||
filename: string;
|
||||
original_filename: string;
|
||||
stored_filename: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -186,12 +192,21 @@ class DocuElevateAPI {
|
||||
return this.request<UploadResponse>("POST", "/api/ui-upload", { formData });
|
||||
}
|
||||
|
||||
/** List recently processed files. */
|
||||
async listFiles(page = 1, pageSize = 20): Promise<FileRecord[]> {
|
||||
return this.request<FileRecord[]>(
|
||||
/** List recently processed files, optionally filtered by filename search. */
|
||||
async listFiles(page = 1, pageSize = 20, search?: string): Promise<FileRecord[]> {
|
||||
let url = `/api/files?page=${page}&per_page=${pageSize}`;
|
||||
if (search) url += `&search=${encodeURIComponent(search)}`;
|
||||
const data = await this.request<{ files: FileRecord[]; pagination: unknown }>("GET", url);
|
||||
return data.files;
|
||||
}
|
||||
|
||||
/** Get the processing status for a single file by its ID. */
|
||||
async getFileStatus(fileId: number): Promise<ProcessingStatus> {
|
||||
const data = await this.request<{ processing_status: ProcessingStatus }>(
|
||||
"GET",
|
||||
`/api/files?page=${page}&page_size=${pageSize}`
|
||||
`/api/files/${fileId}`
|
||||
);
|
||||
return data.processing_status;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user