tacel-file-attachments
v1.1.4
Published
A reusable file attachments component for Electron apps. Supports upload, preview, download, delete, rename, tagging, and per-user/per-file permissions.
Maintainers
Readme
tacel-file-attachments
A reusable file attachments component for Electron apps. Attach files to any database record with upload, preview, download, delete, rename, tagging, language selection, and granular per-user/per-file permissions.
No sensitive data is stored in this module. All database credentials, table names, storage paths, and app-specific configuration are passed in at runtime by the consuming application.
Table of Contents
- Installation
- Architecture Overview
- Frontend Usage
- Backend Usage
- API Methods
- File Data Shape
- Filter Options
- Theming (CSS Variables)
- Database Setup (App Responsibility)
- Storage Setup (App Responsibility)
- Security
- Examples
- Version History
Installation
npm install tacel-file-attachmentsArchitecture Overview
This module has two parts:
| Part | File | Process | Purpose |
|------|------|---------|---------|
| Frontend | file-attachments.js + file-attachments.css | Renderer | UI component — renders file lists, action icons, upload controls, preview |
| Backend | file-attachments-api.js | Main | IPC handler factory — registers Electron IPC handlers for file CRUD operations |
The frontend communicates with the backend via Electron IPC using a configurable channel prefix to avoid conflicts across apps.
┌─────────────────────────────────────────────────┐
│ Renderer Process (Frontend) │
│ │
│ new FileAttachments(container, { │
│ channelPrefix: 'my-app-files', │
│ ipcInvoke: window.electron.ipcRenderer.invoke│
│ }) │
│ │ │
│ │ IPC invoke('my-app-files:list', ...) │
│ ▼ │
├─────────────────────────────────────────────────┤
│ Main Process (Backend) │
│ │
│ createFileAttachmentsAPI(ipcMain, { │
│ channelPrefix: 'my-app-files', │
│ dbQuery: myPool.execute.bind(myPool), │
│ tableName: 'file_attachments', │
│ getStoragePath: (type, id, name) => '...' │
│ }) │
└─────────────────────────────────────────────────┘Frontend Usage
In Electron Renderer
<!-- Include the CSS -->
<link rel="stylesheet" href="node_modules/tacel-file-attachments/file-attachments.css">
<!-- Include the JS -->
<script src="node_modules/tacel-file-attachments/file-attachments.js"></script>// Create an instance
const attachments = new FileAttachments(document.getElementById('my-container'), {
// Required
channelPrefix: 'my-app-files',
ipcInvoke: window.electron.ipcRenderer.invoke,
entityType: 'warranty_terms',
entityId: 42,
currentUser: 'Keith'
});In Node.js (require)
const { FileAttachments } = require('tacel-file-attachments');Constructor Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| channelPrefix | string | 'file-attach' | IPC channel prefix. Must match the backend. |
| ipcInvoke | function | required | Reference to window.electron.ipcRenderer.invoke |
| entityType | string | required | Entity/page name (e.g. 'warranty_terms', 'products') |
| entityId | number\|string | required | The specific row ID this instance is attached to |
| currentUser | string | '' | Current logged-in username (used for permission checks) |
Upload Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| allowUpload | boolean | true | Enable upload functionality |
| uploadMode | string | 'both' | 'click' (browse button), 'drag' (drop zone), or 'both' |
| allowBulkUpload | boolean | true | Allow selecting multiple files at once |
| maxFileSize | number | 52428800 | Max file size in bytes (default 50 MB). App can override. |
| blockedExtensions | string[] | ['exe','bat','cmd','sh','msi','com','scr','pif','vbs','js'] | File extensions that are always blocked |
| allowedExtensions | string[] | null | If set, only these extensions are allowed (overrides blockedExtensions) |
Action Icons
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| actions | string[] | ['preview','open','download','delete'] | Which action icons to show per file. Available: 'preview', 'open', 'download', 'print', 'edit', 'delete', 'rename' |
| allowRename | boolean | false | Shorthand to include 'rename' in actions |
| allowPrint | boolean | false | Shorthand to include 'print' in actions |
Each action renders as a small icon button next to the file. The consuming app picks which actions to show.
Permission Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| readOnly | boolean | false | Global read-only mode. Disables upload, delete, rename for all users. |
| editableUsers | string[] | null | Full-edit users: can upload, delete, rename, edit details. null = all users can. |
| uploadOnlyUsers | string[] | null | These users can view and upload, but cannot delete, rename, or edit. |
| viewOnlyUsers | string[] | null | These users are always read-only (overrides editableUsers and uploadOnlyUsers). |
| canDelete | function(file, user) | null | Custom per-file delete permission check. Return false to block. |
| canRename | function(file, user) | null | Custom per-file rename permission check. Return false to block. |
Per-file locking: Files with locked: true in their data cannot be deleted regardless of user permissions.
3-Tier Permission Model:
| Tier | Can View/Download | Can Upload | Can Edit/Delete/Rename |
|------|:-:|:-:|:-:|
| View-only (viewOnlyUsers) | ✅ | ❌ | ❌ |
| Upload-only (uploadOnlyUsers) | ✅ | ✅ | ❌ |
| Full edit (editableUsers or default) | ✅ | ✅ | ✅ |
Permission priority (highest to lowest):
file.locked === true→ always undeletableviewOnlyUsers→ view/download onlyuploadOnlyUsers→ view + upload onlyreadOnly === true→ all read-onlyeditableUsers→ only listed users get full editcanDelete(file, user)/canRename(file, user)→ custom logic- Default: all actions allowed
Tags & Language
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| showTags | boolean | false | Enable tag selection on files |
| availableTags | string[] | [] | Tag options the user can pick from |
| showLanguage | boolean | false | Enable language selector (English / French / None) |
| languageOptions | object[] | [{value:'EN',label:'English'},{value:'FR',label:'French'}] | Language choices |
Preview Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| enablePreview | boolean | true | Enable the preview action icon |
| onRenderPreview | function(previewEl, file) | null | App controls where/how to display the preview. Module calls this with a DOM element containing the rendered preview and the file metadata. If null, module opens a default centered overlay. |
Supported preview formats:
- Images: PNG, JPEG, JPG, GIF, BMP, WEBP, SVG, TIFF
- PDF: Rendered in an embedded
<iframe>or<embed> - Text/CSV: Rendered as preformatted text
- Word (.docx): Basic text extraction preview
- Excel (.xlsx, .xls): Basic table preview
- Unsupported types: Shows file info with a "Download to view" message
Edit Details
The 'edit' action opens a dialog where users can modify a file's name, tags, and language in one place. This replaces the standalone 'rename' action for a more complete editing experience.
actions: ['preview', 'open', 'download', 'edit', 'delete']The edit dialog features a hybrid tag input — users can click suggested tags or type custom ones, shown as removable chips.
Filter Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| showFilters | boolean | false | Enable the filter bar above the file list |
| filters | string[] | [] | Which filters to show. The app picks only the ones it needs. |
Available filters:
| Filter Key | Description |
|------------|-------------|
| 'search' | Free-text search across file name, tags, and uploaded_by |
| 'dateRange' | Filter by upload date (from/to date pickers) |
| 'fileType' | Filter by category: Images, Documents, PDFs, Spreadsheets, etc. |
| 'fileSize' | Filter by size bucket: Small (<100KB), Medium, Large, Very Large |
| 'tags' | Toggle tag buttons — files must have ALL selected tags |
| 'language' | Filter by language |
| 'uploadedBy' | Filter by uploader (dynamically populated from file list) |
| 'locked' | Filter by locked/unlocked status |
| 'extension' | Filter by specific file extension (e.g. "pdf", "docx") |
| 'sort' | Sort by name, date, size, or type (ascending/descending) |
Filter bar UI:
- Search input always visible at top
- "Filters" toggle button expands/collapses the advanced panel
- Active filter count badge + "Clear All" button
- Removable filter chips for each active filter
- File count display (e.g. "3 of 12 files")
// Minimal: just search and sort
new FileAttachments(container, {
showFilters: true,
filters: ['search', 'sort']
});
// Full: all filters enabled
new FileAttachments(container, {
showFilters: true,
filters: ['search', 'dateRange', 'fileType', 'fileSize', 'tags', 'language', 'uploadedBy', 'locked', 'extension', 'sort']
});Display Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| showUploadedBy | boolean | false | Show who uploaded each file |
| showFileSize | boolean | true | Show file size (formatted: KB, MB) |
| showDate | boolean | true | Show upload date |
| showThumbnail | boolean | true | Show file type icon/thumbnail |
| emptyMessage | string | 'No files attached' | Message when no files exist |
| dateFormat | function(dateStr) | Built-in formatter | Custom date formatting function |
Callbacks
| Callback | Signature | Description |
|----------|-----------|-------------|
| onUpload | (file) => void | Called after a successful upload. Receives the new file record. |
| onDelete | (file) => void | Called after a successful delete. Receives the deleted file record. |
| onView | (file) => void | Called when view/open is triggered. |
| onDownload | (file) => void | Called when download is triggered. |
| onRename | (file, newName) => void | Called after a successful rename. |
| onError | (error, action) => void | Called on any error. action is the operation that failed. |
| confirmDelete | (file) => Promise<boolean> | Override the delete confirmation dialog. Return true to proceed, false to cancel. If not provided, module shows a built-in confirm dialog. |
Backend Usage
// In your Electron main process
const { createFileAttachmentsAPI } = require('tacel-file-attachments/file-attachments-api');
createFileAttachmentsAPI(ipcMain, {
channelPrefix: 'my-app-files',
dbQuery: async (sql, params) => { /* your DB query function */ },
tableName: 'file_attachments',
getStoragePath: (entityType, entityId, fileName) => {
// Return the full file path — YOU control the folder structure
return path.join('\\\\server\\share', entityType, String(entityId), fileName);
}
});API Factory Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| channelPrefix | string | 'file-attach' | Must match the frontend prefix |
| dbQuery | function(sql, params) | required | Your app's DB query function. Must return rows for SELECT, { insertId } for INSERT. |
| tableName | string | required | Your chosen table name (e.g. 'file_attachments') |
| getStoragePath | function(entityType, entityId, fileName) | required | Returns the full absolute file path for storage. App controls all folder structure. |
| onAfterUpload | function(record) | null | Optional hook called after a file is saved to disk and DB |
| onAfterDelete | function(record) | null | Optional hook called after a file is deleted from disk and DB |
IPC Channels Registered
Using prefix my-app-files as an example:
| Channel | Args | Returns | Description |
|---------|------|---------|-------------|
| my-app-files:list | { entityType, entityId } | { ok, files[] } | List all files for an entity |
| my-app-files:upload | { entityType, entityId, fileName, fileData, mimeType, tags?, language?, uploadedBy? } | { ok, file } | Upload a file (fileData is base64) |
| my-app-files:download | { id } | { ok, fileName, fileData, mimeType } | Get file content as base64 for download |
| my-app-files:open | { id } | { ok } | Open file with system default application |
| my-app-files:delete | { id } | { ok } | Delete file from disk and DB |
| my-app-files:rename | { id, newName } | { ok, file } | Rename file display name in DB |
| my-app-files:preview | { id } | { ok, fileName, fileData, mimeType } | Get file content for in-app preview |
| my-app-files:update-meta | { id, tags?, language? } | { ok } | Update tags and/or language on a file |
API Methods
const attachments = new FileAttachments(container, options);
// Refresh the file list (re-fetch from backend)
attachments.refresh();
// Switch to a different entity (e.g. when user opens a different row)
attachments.setEntity('products', 15);
// Get the current file list (already loaded)
const files = attachments.getFiles();
// Render a preview for a specific file (returns a DOM element)
const previewEl = attachments.renderPreview(file);
// Programmatically trigger upload dialog
attachments.openUploadDialog();
// Clean up all event listeners and DOM
attachments.destroy();File Data Shape
Each file record has this shape (returned by getFiles() and passed to callbacks):
{
id: 42, // Auto-increment ID from DB
entity_type: 'warranty_terms', // Which page/table
entity_id: 7, // Which row
file_name: 'warranty-doc.pdf', // Display name
file_path: '...', // Full disk path (backend only, not exposed to frontend)
file_size: 245000, // Size in bytes
mime_type: 'application/pdf', // MIME type
tags: 'contract,important', // Comma-separated tags (or null)
language: 'EN', // 'EN', 'FR', or null
uploaded_by: 'Keith', // Who uploaded it
locked: false, // If true, cannot be deleted
created_at: '2026-02-06T...' // Upload timestamp
}Theming (CSS Variables)
The module ships with neutral default styles. Override these CSS variables in your app to match your theme:
/* In your app's CSS */
.fa-attachments {
/* Colors */
--fa-primary: #6366f1; /* Primary accent (buttons, links) */
--fa-primary-hover: #4f46e5; /* Primary hover state */
--fa-danger: #ef4444; /* Delete / error color */
--fa-danger-hover: #dc2626; /* Delete hover */
--fa-success: #22c55e; /* Success color */
--fa-warning: #f59e0b; /* Warning color */
/* Backgrounds */
--fa-bg: #ffffff; /* Component background */
--fa-bg-hover: #f8fafc; /* Row hover background */
--fa-bg-dropzone: #f1f5f9; /* Upload drop zone background */
--fa-bg-dropzone-active: #e0e7ff;/* Drop zone when dragging over */
--fa-bg-preview: #ffffff; /* Preview overlay background */
--fa-bg-confirm: #ffffff; /* Confirm dialog background */
/* Borders */
--fa-border: #e2e8f0; /* Default border color */
--fa-border-radius: 8px; /* Border radius */
--fa-border-dropzone: #cbd5e1; /* Drop zone border */
/* Text */
--fa-text: #1e293b; /* Primary text */
--fa-text-muted: #64748b; /* Secondary/muted text */
--fa-text-inverse: #ffffff; /* Text on colored backgrounds */
/* Typography */
--fa-font: inherit; /* Font family */
--fa-font-size: 13px; /* Base font size */
--fa-font-size-sm: 11px; /* Small text (file size, date) */
/* Sizing */
--fa-icon-size: 16px; /* Action icon size */
--fa-row-height: 36px; /* File row height */
--fa-thumbnail-size: 28px; /* File type icon/thumbnail size */
/* Shadows */
--fa-shadow: 0 1px 3px rgba(0,0,0,0.1); /* Default shadow */
--fa-shadow-hover: 0 4px 12px rgba(0,0,0,0.15); /* Hover shadow */
--fa-shadow-overlay: 0 20px 60px rgba(0,0,0,0.3); /* Overlay shadow */
/* Transitions */
--fa-transition: 150ms ease; /* Default transition duration */
}Example: Office-HQ Theme (Gold)
.fa-attachments {
--fa-primary: #d4a843;
--fa-primary-hover: #c49a3a;
--fa-border: #e0d5b8;
}Example: ShipWorks Theme (Blue)
.fa-attachments {
--fa-primary: #1a73e8;
--fa-primary-hover: #1557b0;
--fa-border: #c2d7f0;
}Example: Tech-Portal Theme
.fa-attachments {
--fa-primary: #10b981;
--fa-primary-hover: #059669;
--fa-border: #a7f3d0;
}Database Setup (App Responsibility)
The module does not create tables or manage schemas. Your app must create the required table. Here is a recommended schema:
CREATE TABLE file_attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id INT NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size INT DEFAULT 0,
mime_type VARCHAR(100) DEFAULT '',
tags VARCHAR(500) DEFAULT NULL,
language VARCHAR(10) DEFAULT NULL,
uploaded_by VARCHAR(100) DEFAULT '',
locked TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_entity (entity_type, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Note: The table name, column names, and database are entirely up to your app. You pass the table name to the backend factory. The module constructs queries using the table name you provide.
Storage Setup (App Responsibility)
The module does not manage folder structure. Your app's getStoragePath function controls exactly where files are stored:
// Example: Network share organized by entity
getStoragePath: (entityType, entityId, fileName) => {
return path.join(
'\\\\traffic2\\Enterprise2\\Shared\\App-Storage',
'office-hq',
'rma-settings',
entityType,
String(entityId),
fileName
);
}The module will:
- Call your
getStoragePathto get the full path - Ensure the parent directory exists (creates it if needed)
- Write the file to that path
- Store the path in the DB record
Security
This module contains zero sensitive data:
| What | In Module? | Where It Lives |
|------|-----------|----------------|
| SQL credentials (host, user, password) | ❌ No | Your app's private config |
| Database names | ❌ No | Passed at runtime via dbQuery |
| Table names or schemas | ❌ No | Passed at runtime via tableName |
| File storage paths | ❌ No | Passed at runtime via getStoragePath |
| User lists or roles | ❌ No | Passed at runtime via options |
| App-specific logic | ❌ No | Handled by your app's callbacks |
The module is safe to publish publicly on npm.
Examples
Minimal Setup (Read-Only File List)
const attachments = new FileAttachments(container, {
channelPrefix: 'my-files',
ipcInvoke: window.electron.ipcRenderer.invoke,
entityType: 'products',
entityId: 5,
readOnly: true,
actions: ['preview', 'download']
});Full-Featured Setup
const attachments = new FileAttachments(container, {
channelPrefix: 'office-hq-files',
ipcInvoke: window.electron.ipcRenderer.invoke,
entityType: 'repair_agreements',
entityId: 12,
currentUser: 'Keith',
// Upload
allowUpload: true,
uploadMode: 'both',
allowBulkUpload: true,
maxFileSize: 25 * 1024 * 1024, // 25 MB
// Actions
actions: ['preview', 'open', 'download', 'rename', 'delete'],
// Permissions
editableUsers: ['Keith', 'test'],
canDelete: (file, user) => !file.locked,
// Tags & Language
showTags: true,
availableTags: ['Contract', 'Invoice', 'Warranty', 'Report'],
showLanguage: true,
// Display
showUploadedBy: true,
showFileSize: true,
showDate: true,
// Preview
enablePreview: true,
onRenderPreview: (previewEl, file) => {
// Open in a custom modal
myApp.openModal(previewEl, { title: file.file_name });
},
// Callbacks
onUpload: (file) => console.log('Uploaded:', file.file_name),
onDelete: (file) => console.log('Deleted:', file.file_name),
onError: (err, action) => console.error(`${action} failed:`, err)
});Switching Entities (e.g., When Modal Changes)
// User opens a different row in the table
function onRowSelected(rowId) {
attachments.setEntity('warranty_terms', rowId);
}Auto-Update on App Start
To ensure you always have the latest version:
const { exec } = require('child_process');
exec('npm update tacel-file-attachments', { cwd: __dirname }, (err) => {
if (err) console.log('Update check failed:', err.message);
else console.log('FileAttachments package up to date');
});Version History
| Version | Date | Changes | |---------|------|---------| | 1.1.0 | 2026-02-06 | Added: comprehensive filter system (10 configurable filters), edit details dialog (name + tags + language), hybrid tag input with chips, 3-tier permission model (view-only / upload-only / full-edit), dynamic "Uploaded By" filter | | 1.0.0 | 2026-02-06 | Initial release — upload, preview, download, delete, rename, tags, language, permissions, theming |
License
MIT © Tacel Ltd
