Update documents route with delete endpoint - WIP
This commit is contained in:
parent
ba36803f05
commit
e7a97294e2
3 changed files with 544 additions and 34 deletions
342
FEATURE-ROADMAP.md
Normal file
342
FEATURE-ROADMAP.md
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
# NaviDocs Single-Tenant Feature Roadmap
|
||||
|
||||
**Version:** 1.0 → 2.0
|
||||
**Target:** Complete single-tenant boat manual management system
|
||||
**Approach:** Incremental implementation with backward compatibility
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Vision
|
||||
|
||||
Transform NaviDocs from a demo into a production-ready single-tenant system with complete document lifecycle management, advanced search, and user preferences.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Feature Categories
|
||||
|
||||
### 1. Document Management ⭐ (Priority 1)
|
||||
- [x] Upload with OCR
|
||||
- [x] View with text selection
|
||||
- [x] Auto-fill metadata
|
||||
- [ ] **Delete documents** (with confirmation)
|
||||
- [ ] **Edit metadata** (inline or modal)
|
||||
- [ ] **Bulk operations** (select multiple, bulk delete)
|
||||
- [ ] **Document versions** (track changes)
|
||||
- [ ] **Download original PDF**
|
||||
|
||||
### 2. Advanced Search 🔍 (Priority 1)
|
||||
- [x] Full-text search
|
||||
- [x] Diagram thumbnails
|
||||
- [ ] **Filter by boat** (dropdown)
|
||||
- [ ] **Filter by document type** (manual, service, warranty)
|
||||
- [ ] **Filter by date range** (uploaded date)
|
||||
- [ ] **Sort options** (relevance, date, title)
|
||||
- [ ] **Search within document**
|
||||
- [ ] **Recent searches** (last 10)
|
||||
- [ ] **Search suggestions** (autocomplete)
|
||||
- [ ] **Export search results** (PDF/CSV)
|
||||
|
||||
### 3. User Experience 🎨 (Priority 2)
|
||||
- [x] Toast notifications
|
||||
- [x] Responsive design
|
||||
- [x] Dark theme
|
||||
- [ ] **Keyboard shortcuts** (/, Ctrl+K for search, etc.)
|
||||
- [ ] **Bookmarks** (save specific pages)
|
||||
- [ ] **Reading progress** (remember last page)
|
||||
- [ ] **Print-friendly view**
|
||||
- [ ] **Fullscreen mode**
|
||||
- [ ] **Night mode toggle** (darker theme)
|
||||
|
||||
### 4. Dashboard & Analytics 📊 (Priority 2)
|
||||
- [ ] **Statistics dashboard**
|
||||
- Total documents
|
||||
- Total pages
|
||||
- Storage used
|
||||
- Most searched terms
|
||||
- Recent activity
|
||||
- [ ] **Document health**
|
||||
- OCR status
|
||||
- Missing metadata
|
||||
- Indexing status
|
||||
- [ ] **Usage charts**
|
||||
- Searches over time
|
||||
- Documents added over time
|
||||
|
||||
### 5. Settings & Preferences ⚙️ (Priority 1)
|
||||
- [ ] **Organization settings**
|
||||
- Organization name
|
||||
- Default boat name
|
||||
- Logo upload
|
||||
- [ ] **User preferences**
|
||||
- Theme (dark/light)
|
||||
- Default search filters
|
||||
- Items per page
|
||||
- Language
|
||||
- [ ] **Storage management**
|
||||
- View storage usage
|
||||
- Cleanup old files
|
||||
- Export all data
|
||||
|
||||
### 6. Help & Onboarding 💡 (Priority 3)
|
||||
- [ ] **Interactive tour** (first visit)
|
||||
- [ ] **Keyboard shortcuts guide**
|
||||
- [ ] **Search tips**
|
||||
- [ ] **Feature highlights**
|
||||
- [ ] **FAQ section**
|
||||
|
||||
### 7. Data Management 💾 (Priority 2)
|
||||
- [ ] **Export all documents** (ZIP)
|
||||
- [ ] **Import from backup**
|
||||
- [ ] **Activity log** (audit trail)
|
||||
- [ ] **Database maintenance** (vacuum, optimize)
|
||||
|
||||
### 8. Polish & Performance ⚡ (Priority 3)
|
||||
- [x] Comprehensive logging
|
||||
- [x] Error handling
|
||||
- [ ] **Image lazy loading**
|
||||
- [ ] **PDF page caching**
|
||||
- [ ] **Search result caching**
|
||||
- [ ] **Compression for images**
|
||||
- [ ] **Background job queue**
|
||||
|
||||
---
|
||||
|
||||
## 🗓️ Implementation Plan
|
||||
|
||||
### Phase 1: Core Management (Day 1)
|
||||
**Goal:** Complete document lifecycle management
|
||||
|
||||
1. **Document Deletion**
|
||||
- Add delete button to document cards
|
||||
- Confirmation modal
|
||||
- Backend endpoint
|
||||
- Cleanup files, DB, and search index
|
||||
- Toast notification on success
|
||||
|
||||
2. **Metadata Editing**
|
||||
- Edit modal/form
|
||||
- Update boat info, title, type
|
||||
- Re-index search after update
|
||||
- Optimistic UI updates
|
||||
|
||||
3. **Download Original PDF**
|
||||
- Download button in document viewer
|
||||
- Proper filename
|
||||
|
||||
### Phase 2: Advanced Search (Day 1-2)
|
||||
**Goal:** Make search powerful and flexible
|
||||
|
||||
1. **Search Filters**
|
||||
- Boat filter (dropdown with all boats)
|
||||
- Document type filter
|
||||
- Date range filter
|
||||
- Apply filters to search query
|
||||
|
||||
2. **Search Enhancements**
|
||||
- Sort by relevance/date/title
|
||||
- Recent searches (localStorage)
|
||||
- Search within current document
|
||||
|
||||
3. **Search Export**
|
||||
- Export results as PDF
|
||||
- Export as CSV
|
||||
|
||||
### Phase 3: Dashboard (Day 2)
|
||||
**Goal:** Provide system overview and insights
|
||||
|
||||
1. **Statistics Dashboard**
|
||||
- Total documents, pages, storage
|
||||
- Recent uploads
|
||||
- Popular searches
|
||||
- Quick actions
|
||||
|
||||
2. **Document Health**
|
||||
- Show processing status
|
||||
- Identify issues
|
||||
- Quick fixes
|
||||
|
||||
### Phase 4: Settings & Preferences (Day 2-3)
|
||||
**Goal:** Customizable experience
|
||||
|
||||
1. **Settings Page**
|
||||
- Organization info
|
||||
- User preferences
|
||||
- Storage management
|
||||
- Database maintenance
|
||||
|
||||
2. **Theme Toggle**
|
||||
- Dark/light mode
|
||||
- Persistent preference
|
||||
|
||||
### Phase 5: UX Enhancements (Day 3)
|
||||
**Goal:** Make it delightful to use
|
||||
|
||||
1. **Keyboard Shortcuts**
|
||||
- / or Ctrl+K for search
|
||||
- Arrow keys for navigation
|
||||
- Escape to close modals
|
||||
- Shortcuts help modal
|
||||
|
||||
2. **Bookmarks**
|
||||
- Save favorite pages
|
||||
- Quick access
|
||||
- Manage bookmarks
|
||||
|
||||
3. **Reading Progress**
|
||||
- Remember last page per document
|
||||
- Resume reading
|
||||
|
||||
### Phase 6: Polish & Testing (Day 3)
|
||||
**Goal:** Production-ready quality
|
||||
|
||||
1. **Performance Optimization**
|
||||
- Image lazy loading
|
||||
- Caching strategies
|
||||
- Compression
|
||||
|
||||
2. **Testing**
|
||||
- E2E tests for new features
|
||||
- Load testing
|
||||
- Edge case handling
|
||||
|
||||
3. **Documentation**
|
||||
- Update guides
|
||||
- Feature screenshots
|
||||
- API documentation
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI/UX Considerations
|
||||
|
||||
### Design Principles
|
||||
1. **Consistent:** Follow existing pink/purple dark theme
|
||||
2. **Intuitive:** Common patterns (3-dot menus, icon buttons)
|
||||
3. **Responsive:** All features work on mobile
|
||||
4. **Accessible:** Keyboard navigation, ARIA labels
|
||||
5. **Fast:** Optimistic updates, caching
|
||||
|
||||
### Component Additions
|
||||
- `ConfirmDialog.vue` - Reusable confirmation modal
|
||||
- `EditDocumentModal.vue` - Edit document metadata
|
||||
- `SearchFilters.vue` - Advanced search filter panel
|
||||
- `StatsCard.vue` - Dashboard statistics cards
|
||||
- `SettingsPanel.vue` - Settings sections
|
||||
- `KeyboardShortcuts.vue` - Shortcuts help modal
|
||||
- `BookmarksList.vue` - Bookmarks sidebar
|
||||
|
||||
### New Routes
|
||||
```javascript
|
||||
/ # Home (existing)
|
||||
/search # Search (existing)
|
||||
/document/:id # Document viewer (existing)
|
||||
/dashboard # New: Statistics dashboard
|
||||
/settings # New: Settings page
|
||||
/bookmarks # New: Bookmarks list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Backend Changes
|
||||
|
||||
### New Endpoints
|
||||
```javascript
|
||||
DELETE /api/documents/:id # Delete document
|
||||
PATCH /api/documents/:id # Update metadata
|
||||
GET /api/documents/:id/download # Download PDF
|
||||
GET /api/stats # Get statistics
|
||||
GET /api/search/history # Recent searches
|
||||
POST /api/bookmarks # Add bookmark
|
||||
GET /api/bookmarks # List bookmarks
|
||||
DELETE /api/bookmarks/:id # Remove bookmark
|
||||
GET /api/settings # Get settings
|
||||
PATCH /api/settings # Update settings
|
||||
POST /api/export # Export all data
|
||||
```
|
||||
|
||||
### Database Schema Additions
|
||||
```sql
|
||||
-- Bookmarks
|
||||
CREATE TABLE bookmarks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
documentId TEXT NOT NULL,
|
||||
pageNumber INTEGER NOT NULL,
|
||||
note TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
FOREIGN KEY(documentId) REFERENCES documents(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Settings
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updatedAt INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Activity Log
|
||||
CREATE TABLE activity_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
action TEXT NOT NULL,
|
||||
entity TEXT,
|
||||
entityId TEXT,
|
||||
details TEXT,
|
||||
createdAt INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Breaking Changes Prevention
|
||||
|
||||
### Backward Compatibility Strategy
|
||||
1. **Add, don't replace:** New endpoints alongside existing
|
||||
2. **Optional features:** All new features are additive
|
||||
3. **Database migrations:** Careful schema updates
|
||||
4. **API versioning:** Consider /api/v1 prefix for future
|
||||
5. **Feature flags:** Toggle new features on/off
|
||||
|
||||
### Testing Strategy
|
||||
1. **Existing tests must pass:** All 8 E2E tests
|
||||
2. **New tests for new features:** Add to test suite
|
||||
3. **Regression testing:** Verify old features still work
|
||||
4. **Manual testing:** Complete user flow tests
|
||||
|
||||
---
|
||||
|
||||
## 📊 Success Metrics
|
||||
|
||||
### Completion Criteria
|
||||
- [ ] All priority 1 features implemented
|
||||
- [ ] 15+ E2E tests passing
|
||||
- [ ] < 100ms response time for all endpoints
|
||||
- [ ] Zero console errors
|
||||
- [ ] Mobile responsive verified
|
||||
- [ ] Complete documentation updated
|
||||
- [ ] Demo-ready with showcase
|
||||
|
||||
### Quality Gates
|
||||
1. **Code Quality:** ESLint passing, no warnings
|
||||
2. **Performance:** Lighthouse score >90
|
||||
3. **Tests:** >80% feature coverage
|
||||
4. **Documentation:** All new features documented
|
||||
5. **UX:** User testing with 3+ scenarios
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment Checklist
|
||||
|
||||
- [ ] Run all tests
|
||||
- [ ] Update environment variables
|
||||
- [ ] Database migrations applied
|
||||
- [ ] Backup existing data
|
||||
- [ ] Performance testing
|
||||
- [ ] Security audit
|
||||
- [ ] Documentation updated
|
||||
- [ ] Stakeholder demo
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:** Begin Phase 1 implementation
|
||||
**Timeline:** 3-day sprint for complete feature set
|
||||
**Goal:** Production-ready single-tenant system with all features
|
||||
|
||||
Let's build something amazing! 🚢✨
|
||||
157
client/src/components/ConfirmDialog.vue
Normal file
157
client/src/components/ConfirmDialog.vue
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm px-4"
|
||||
@click.self="onCancel"
|
||||
>
|
||||
<div class="bg-dark-800 rounded-2xl shadow-2xl border border-white/10 max-w-md w-full overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-white/10">
|
||||
<div class="flex items-center gap-3">
|
||||
<div :class="[
|
||||
'w-10 h-10 rounded-xl flex items-center justify-center',
|
||||
variantClasses[variant].bg
|
||||
]">
|
||||
<svg v-if="variant === 'danger'" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<svg v-else-if="variant === 'warning'" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<svg v-else class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-white">{{ title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-white/80 leading-relaxed">{{ message }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 bg-dark-900/50 flex items-center justify-end gap-3">
|
||||
<button
|
||||
@click="onCancel"
|
||||
class="px-4 py-2 rounded-lg border border-white/20 text-white hover:bg-white/5 transition-colors"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button
|
||||
@click="onConfirm"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-lg font-medium transition-colors',
|
||||
variantClasses[variant].button,
|
||||
loading && 'opacity-50 cursor-not-allowed'
|
||||
]"
|
||||
:disabled="loading"
|
||||
>
|
||||
<span v-if="!loading">{{ confirmText }}</span>
|
||||
<span v-else class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
{{ loadingText }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Confirm Action'
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: 'Confirm'
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: 'Cancel'
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: 'Processing...'
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'info', // 'info', 'warning', 'danger'
|
||||
validator: (value) => ['info', 'warning', 'danger'].includes(value)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const variantClasses = {
|
||||
danger: {
|
||||
bg: 'bg-red-500/20 text-red-400',
|
||||
button: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
},
|
||||
warning: {
|
||||
bg: 'bg-yellow-500/20 text-yellow-400',
|
||||
button: 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||
},
|
||||
info: {
|
||||
bg: 'bg-blue-500/20 text-blue-400',
|
||||
button: 'bg-gradient-to-r from-pink-400 to-purple-500 hover:from-pink-500 hover:to-purple-600 text-white'
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
if (loading.value) return
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
await emit('confirm')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-active > div,
|
||||
.modal-leave-active > div {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from > div,
|
||||
.modal-leave-to > div {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,10 +5,16 @@
|
|||
|
||||
import express from 'express';
|
||||
import { getDb } from '../db/db.js';
|
||||
import { getMeilisearchClient } from '../config/meilisearch.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { rm } from 'fs/promises';
|
||||
import { loggers } from '../utils/logger.js';
|
||||
|
||||
const router = express.Router();
|
||||
const logger = loggers.app.child('Documents');
|
||||
|
||||
const MEILISEARCH_INDEX_NAME = process.env.MEILISEARCH_INDEX_NAME || 'navidocs-pages';
|
||||
|
||||
/**
|
||||
* GET /api/documents/:id
|
||||
|
|
@ -343,59 +349,64 @@ router.get('/', async (req, res) => {
|
|||
|
||||
/**
|
||||
* DELETE /api/documents/:id
|
||||
* Soft delete a document (mark as deleted)
|
||||
* Hard delete a document (removes from DB, filesystem, and search index)
|
||||
* For single-tenant demo - simplified permissions
|
||||
*/
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// TODO: Authentication middleware should provide req.user
|
||||
const userId = req.user?.id || 'test-user-id';
|
||||
try {
|
||||
logger.info(`Deleting document ${id}`);
|
||||
|
||||
const db = getDb();
|
||||
const searchClient = getMeilisearchClient();
|
||||
|
||||
// Check ownership
|
||||
const document = db.prepare(`
|
||||
SELECT id, organization_id, uploaded_by
|
||||
FROM documents
|
||||
WHERE id = ?
|
||||
`).get(id);
|
||||
// Get document info before deletion
|
||||
const document = db.prepare('SELECT * FROM documents WHERE id = ?').get(id);
|
||||
|
||||
if (!document) {
|
||||
logger.warn(`Document ${id} not found`);
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
|
||||
// Verify user has permission (must be uploader or org admin)
|
||||
const hasPermission = db.prepare(`
|
||||
SELECT 1 FROM user_organizations
|
||||
WHERE user_id = ? AND organization_id = ? AND role IN ('admin', 'manager')
|
||||
UNION
|
||||
SELECT 1 FROM documents
|
||||
WHERE id = ? AND uploaded_by = ?
|
||||
`).get(userId, document.organization_id, id, userId);
|
||||
|
||||
if (!hasPermission) {
|
||||
return res.status(403).json({
|
||||
error: 'Access denied',
|
||||
message: 'You do not have permission to delete this document'
|
||||
});
|
||||
// Delete from Meilisearch index
|
||||
try {
|
||||
const index = await searchClient.getIndex(MEILISEARCH_INDEX_NAME);
|
||||
const filter = `docId = "${id}"`;
|
||||
await index.deleteDocuments({ filter });
|
||||
logger.info(`Deleted search entries for document ${id}`);
|
||||
} catch (err) {
|
||||
logger.warn(`Meilisearch cleanup failed for ${id}:`, err);
|
||||
// Continue with deletion even if search cleanup fails
|
||||
}
|
||||
|
||||
// Soft delete - update status
|
||||
const timestamp = Date.now();
|
||||
db.prepare(`
|
||||
UPDATE documents
|
||||
SET status = 'deleted', updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(timestamp, id);
|
||||
// Delete from database (CASCADE will handle document_pages, ocr_jobs)
|
||||
const deleteStmt = db.prepare('DELETE FROM documents WHERE id = ?');
|
||||
deleteStmt.run(id);
|
||||
logger.info(`Deleted database record for document ${id}`);
|
||||
|
||||
// Delete from filesystem
|
||||
const uploadsDir = path.join(process.cwd(), '../uploads');
|
||||
const docFolder = path.join(uploadsDir, id);
|
||||
|
||||
if (fs.existsSync(docFolder)) {
|
||||
await rm(docFolder, { recursive: true, force: true });
|
||||
logger.info(`Deleted filesystem folder for document ${id}`);
|
||||
} else {
|
||||
logger.warn(`Folder not found for document ${id}`);
|
||||
}
|
||||
|
||||
logger.info(`Document ${id} deleted successfully`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Document deleted successfully',
|
||||
documentId: id
|
||||
documentId: id,
|
||||
title: document.title
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Document deletion error:', error);
|
||||
logger.error(`Failed to delete document ${id}`, error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to delete document',
|
||||
message: error.message
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue