Episode 13: File Upload and Management
Enabling Secure Document Handling and Digital Asset Management in Your Portal
Introduction
Modern business applications require robust file handling capabilities for documents, images, contracts, and other digital assets. Power Pages provides sophisticated file upload functionality that integrates seamlessly with Dataverse, enabling secure storage, automated processing, and comprehensive file management. This episode teaches you how to implement file uploads with proper security, validation, and user experience considerations.
Key Concepts
- File Upload Security: Implementing proper file type validation and security scanning
- Storage Integration: Connecting uploads to Dataverse attachment entities and Azure storage
- User Experience Design: Creating intuitive drag-and-drop interfaces with progress feedback
- File Processing: Automated workflows for file validation, conversion, and organization
- Access Control: Managing who can upload, view, and download specific files
Learning Objectives
By the end of this episode, you will be able to:
- Configure secure file upload forms with proper validation and restrictions
- Implement drag-and-drop file interfaces with modern UX patterns
- Set up automated file processing and workflow triggers
- Create file management dashboards for users to organize their uploads
- Implement proper security controls for file access and sharing
Why File Management Matters
Professional portals must handle documents, images, and other files securely and efficiently. Poor file handling creates security vulnerabilities, storage bloat, and frustrated users. Proper implementation ensures security compliance, optimal performance, and excellent user experience while enabling powerful document-driven business processes.
Step-by-Step Guide
1. Configuring Secure File Upload Forms
Setting Up Basic File Upload Entity:
First, create a custom entity to track file metadata:
// File Upload Entity Configuration
const fileUploadEntity = {
schemaName: 'cr5f3_fileupload',
displayName: 'File Upload',
fields: [
{
name: 'cr5f3_filename',
type: 'string',
displayName: 'File Name',
required: true,
maxLength: 255
},
{
name: 'cr5f3_filesize',
type: 'integer',
displayName: 'File Size (bytes)',
required: true
},
{
name: 'cr5f3_filetype',
type: 'string',
displayName: 'File Type',
required: true,
maxLength: 50
},
{
name: 'cr5f3_uploaddate',
type: 'datetime',
displayName: 'Upload Date',
required: true
},
{
name: 'cr5f3_uploadedby',
type: 'lookup',
displayName: 'Uploaded By',
target: 'contact',
required: true
},
{
name: 'cr5f3_status',
type: 'optionset',
displayName: 'Status',
options: [
{ value: 1, label: 'Pending' },
{ value: 2, label: 'Validated' },
{ value: 3, label: 'Approved' },
{ value: 4, label: 'Rejected' }
]
},
{
name: 'cr5f3_category',
type: 'optionset',
displayName: 'Category',
options: [
{ value: 1, label: 'Document' },
{ value: 2, label: 'Image' },
{ value: 3, label: 'Contract' },
{ value: 4, label: 'Report' },
{ value: 5, label: 'Other' }
]
}
]
};
Advanced File Upload Form HTML:
<!-- Advanced File Upload Form -->
<div class="file-upload-container" id="fileUploadContainer">
<form class="file-upload-form" id="fileUploadForm" enctype="multipart/form-data">
<!-- Upload Area -->
<div class="upload-zone" id="uploadZone">
<div class="upload-zone-content">
<div class="upload-icon">📁</div>
<h3>Drag & Drop Files Here</h3>
<p>or <button type="button" class="btn-link" id="browseFiles">browse files</button></p>
<div class="upload-restrictions">
<small>Supported formats: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG, GIF</small>
<small>Maximum file size: 10MB per file</small>
<small>Maximum 5 files per upload</small>
</div>
</div>
<input type="file" id="fileInput" name="files" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif" style="display: none;">
</div>
<!-- File Preview Area -->
<div class="file-preview-area" id="filePreviewArea" style="display: none;">
<h4>Selected Files</h4>
<div class="file-list" id="fileList"></div>
<!-- Bulk Actions -->
<div class="bulk-actions">
<div class="form-group">
<label for="bulkCategory">Category for all files:</label>
<select id="bulkCategory" name="bulkCategory">
<option value="">Select category</option>
<option value="1">Document</option>
<option value="2">Image</option>
<option value="3">Contract</option>
<option value="4">Report</option>
<option value="5">Other</option>
</select>
</div>
<div class="form-group">
<label for="bulkDescription">Description (optional):</label>
<textarea id="bulkDescription" name="bulkDescription" rows="3"
placeholder="Add a description for these files..."></textarea>
</div>
</div>
<!-- Upload Controls -->
<div class="upload-controls">
<button type="button" class="btn btn-secondary" id="clearFiles">
Clear All
</button>
<button type="submit" class="btn btn-primary" id="uploadFiles" disabled>
<span class="btn-text">Upload Files</span>
<span class="btn-spinner" style="display: none;">⟳</span>
</button>
</div>
</div>
<!-- Upload Progress -->
<div class="upload-progress-container" id="uploadProgressContainer" style="display: none;">
<h4>Upload Progress</h4>
<div class="overall-progress">
<div class="progress-bar">
<div class="progress-fill" id="overallProgressFill" style="width: 0%"></div>
</div>
<span class="progress-text" id="overallProgressText">0%</span>
</div>
<div class="file-progress-list" id="fileProgressList"></div>
</div>
<!-- Upload Results -->
<div class="upload-results" id="uploadResults" style="display: none;">
<div class="results-summary" id="resultsSummary"></div>
<div class="results-details" id="resultsDetails"></div>
<button type="button" class="btn btn-primary" id="uploadMore">
Upload More Files
</button>
</div>
</form>
</div>
2. Advanced File Upload JavaScript
Comprehensive File Upload Handler:
class AdvancedFileUploadHandler {
constructor(containerSelector) {
this.container = document.querySelector(containerSelector);
this.uploadZone = this.container.querySelector('#uploadZone');
this.fileInput = this.container.querySelector('#fileInput');
this.filePreviewArea = this.container.querySelector('#filePreviewArea');
this.fileList = this.container.querySelector('#fileList');
this.uploadForm = this.container.querySelector('#fileUploadForm');
this.files = [];
this.maxFileSize = 10 * 1024 * 1024; // 10MB
this.maxFiles = 5;
this.allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'image/jpeg',
'image/jpg',
'image/png',
'image/gif'
];
this.setupEventListeners();
this.setupDragAndDrop();
}
setupEventListeners() {
// Browse files button
this.container.querySelector('#browseFiles').addEventListener('click', () => {
this.fileInput.click();
});
// File input change
this.fileInput.addEventListener('change', (e) => {
this.handleFileSelection(Array.from(e.target.files));
});
// Form submission
this.uploadForm.addEventListener('submit', (e) => {
e.preventDefault();
this.startUpload();
});
// Clear files
this.container.querySelector('#clearFiles').addEventListener('click', () => {
this.clearFiles();
});
// Upload more
this.container.querySelector('#uploadMore').addEventListener('click', () => {
this.resetUploadForm();
});
}
setupDragAndDrop() {
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this.uploadZone.addEventListener(eventName, this.preventDefaults, false);
document.body.addEventListener(eventName, this.preventDefaults, false);
});
// Highlight drop zone when item is dragged over it
['dragenter', 'dragover'].forEach(eventName => {
this.uploadZone.addEventListener(eventName, () => {
this.uploadZone.classList.add('drag-over');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
this.uploadZone.addEventListener(eventName, () => {
this.uploadZone.classList.remove('drag-over');
}, false);
});
// Handle dropped files
this.uploadZone.addEventListener('drop', (e) => {
const droppedFiles = Array.from(e.dataTransfer.files);
this.handleFileSelection(droppedFiles);
}, false);
}
preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
handleFileSelection(newFiles) {
const validFiles = [];
const errors = [];
// Validate each file
newFiles.forEach(file => {
const validation = this.validateFile(file);
if (validation.valid) {
validFiles.push(file);
} else {
errors.push(`${file.name}: ${validation.error}`);
}
});
// Check total file count
if (this.files.length + validFiles.length > this.maxFiles) {
errors.push(`Maximum ${this.maxFiles} files allowed. Current: ${this.files.length}, Trying to add: ${validFiles.length}`);
return;
}
// Add valid files
validFiles.forEach(file => {
file.id = this.generateFileId();
file.category = '';
file.description = '';
this.files.push(file);
});
// Show errors if any
if (errors.length > 0) {
this.showErrors(errors);
}
// Update UI
this.updateFilePreview();
this.updateUploadButton();
}
validateFile(file) {
// Check file type
if (!this.allowedTypes.includes(file.type)) {
return {
valid: false,
error: 'File type not allowed. Supported formats: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG, GIF'
};
}
// Check file size
if (file.size > this.maxFileSize) {
return {
valid: false,
error: `File size exceeds ${this.formatFileSize(this.maxFileSize)} limit`
};
}
// Check for duplicates
const duplicate = this.files.find(existingFile =>
existingFile.name === file.name && existingFile.size === file.size
);
if (duplicate) {
return {
valid: false,
error: 'File already selected'
};
}
return { valid: true };
}
updateFilePreview() {
if (this.files.length === 0) {
this.filePreviewArea.style.display = 'none';
return;
}
this.filePreviewArea.style.display = 'block';
this.fileList.innerHTML = '';
this.files.forEach(file => {
const fileItem = this.createFilePreviewItem(file);
this.fileList.appendChild(fileItem);
});
}
createFilePreviewItem(file) {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.dataset.fileId = file.id;
fileItem.innerHTML = `
<div class="file-info">
<div class="file-icon">${this.getFileIcon(file.type)}</div>
<div class="file-details">
<div class="file-name">${file.name}</div>
<div class="file-meta">
${this.formatFileSize(file.size)} • ${this.getFileTypeLabel(file.type)}
</div>
</div>
</div>
<div class="file-controls">
<div class="file-fields">
<select class="file-category" data-file-id="${file.id}">
<option value="">Select category</option>
<option value="1">Document</option>
<option value="2">Image</option>
<option value="3">Contract</option>
<option value="4">Report</option>
<option value="5">Other</option>
</select>
<input type="text" class="file-description" data-file-id="${file.id}"
placeholder="Description (optional)" maxlength="500">
</div>
<button type="button" class="btn-remove" data-file-id="${file.id}">
✕
</button>
</div>
`;
// Add event listeners
const removeBtn = fileItem.querySelector('.btn-remove');
removeBtn.addEventListener('click', () => {
this.removeFile(file.id);
});
const categorySelect = fileItem.querySelector('.file-category');
categorySelect.addEventListener('change', (e) => {
file.category = e.target.value;
});
const descriptionInput = fileItem.querySelector('.file-description');
descriptionInput.addEventListener('input', (e) => {
file.description = e.target.value;
});
return fileItem;
}
async startUpload() {
if (this.files.length === 0) return;
// Show progress container
this.container.querySelector('#uploadProgressContainer').style.display = 'block';
this.uploadForm.style.display = 'none';
const results = [];
let completedFiles = 0;
// Upload files sequentially to avoid overwhelming the server
for (const file of this.files) {
try {
const result = await this.uploadSingleFile(file, (progress) => {
this.updateFileProgress(file.id, progress);
});
results.push({ file, result, success: true });
} catch (error) {
results.push({ file, error, success: false });
}
completedFiles++;
this.updateOverallProgress((completedFiles / this.files.length) * 100);
}
// Show results
this.showUploadResults(results);
}
async uploadSingleFile(file, progressCallback) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);
formData.append('category', file.category || '5'); // Default to 'Other'
formData.append('description', file.description || '');
formData.append('uploadedBy', this.getCurrentContactId());
const xhr = new XMLHttpRequest();
// Track upload progress
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressCallback(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (error) {
reject(new Error('Invalid response from server'));
}
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Upload failed due to network error'));
});
xhr.open('POST', '/api/upload');
xhr.setRequestHeader('X-CSRF-TOKEN', this.getCSRFToken());
xhr.send(formData);
});
}
updateFileProgress(fileId, progress) {
const progressElement = document.getElementById(`file-progress-${fileId}`);
if (progressElement) {
const progressBar = progressElement.querySelector('.progress-fill');
const progressText = progressElement.querySelector('.progress-text');
progressBar.style.width = `${progress}%`;
progressText.textContent = `${Math.round(progress)}%`;
if (progress === 100) {
progressElement.classList.add('completed');
}
}
}
updateOverallProgress(progress) {
const progressFill = this.container.querySelector('#overallProgressFill');
const progressText = this.container.querySelector('#overallProgressText');
progressFill.style.width = `${progress}%`;
progressText.textContent = `${Math.round(progress)}%`;
}
showUploadResults(results) {
const successCount = results.filter(r => r.success).length;
const errorCount = results.filter(r => !r.success).length;
const resultsContainer = this.container.querySelector('#uploadResults');
const summary = this.container.querySelector('#resultsSummary');
const details = this.container.querySelector('#resultsDetails');
// Hide progress, show results
this.container.querySelector('#uploadProgressContainer').style.display = 'none';
resultsContainer.style.display = 'block';
// Summary
summary.innerHTML = `
<div class="results-summary-content">
<h4>Upload Complete</h4>
<div class="summary-stats">
<div class="stat success">
<span class="stat-number">${successCount}</span>
<span class="stat-label">Successful</span>
</div>
${errorCount > 0 ? `
<div class="stat error">
<span class="stat-number">${errorCount}</span>
<span class="stat-label">Failed</span>
</div>
` : ''}
</div>
</div>
`;
// Details
details.innerHTML = results.map(result => `
<div class="result-item ${result.success ? 'success' : 'error'}">
<div class="result-icon">${result.success ? '✓' : '✗'}</div>
<div class="result-details">
<div class="result-filename">${result.file.name}</div>
<div class="result-message">
${result.success ? 'Upload successful' : result.error.message}
</div>
${result.success && result.result.fileId ? `
<div class="result-actions">
<a href="/file/${result.result.fileId}" target="_blank" class="btn-link">
View File
</a>
</div>
` : ''}
</div>
</div>
`).join('');
}
}
3. File Management Dashboard
User File Management Interface:
<!-- File Management Dashboard -->
<div class="file-management-dashboard">
<div class="dashboard-header">
<h2>My Files</h2>
<div class="dashboard-actions">
<button class="btn btn-primary" onclick="openUploadModal()">
📤 Upload Files
</button>
<div class="view-toggle">
<button class="btn btn-outline active" data-view="grid">⊞ Grid</button>
<button class="btn btn-outline" data-view="list">☰ List</button>
</div>
</div>
</div>
<div class="dashboard-filters">
<div class="filter-group">
<label for="categoryFilter">Category:</label>
<select id="categoryFilter" class="filter-select">
<option value="">All Categories</option>
<option value="1">Documents</option>
<option value="2">Images</option>
<option value="3">Contracts</option>
<option value="4">Reports</option>
<option value="5">Other</option>
</select>
</div>
<div class="filter-group">
<label for="statusFilter">Status:</label>
<select id="statusFilter" class="filter-select">
<option value="">All Status</option>
<option value="1">Pending</option>
<option value="2">Validated</option>
<option value="3">Approved</option>
<option value="4">Rejected</option>
</select>
</div>
<div class="filter-group">
<label for="dateFilter">Date Range:</label>
<select id="dateFilter" class="filter-select">
<option value="">All Time</option>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 3 months</option>
</select>
</div>
<div class="search-group">
<input type="text" id="searchFiles" placeholder="Search files..." class="search-input">
<button type="button" class="search-btn">🔍</button>
</div>
</div>
<div class="files-container" id="filesContainer">
<div class="files-grid" id="filesGrid">
<!-- Files will be populated here -->
</div>
</div>
<div class="pagination-container" id="paginationContainer">
<!-- Pagination will be populated here -->
</div>
</div>
4. Server-Side File Processing
Power Automate Flow for File Processing:
{
"definition": {
"triggers": {
"when_file_uploaded": {
"type": "PowerPages",
"kind": "FileUploaded",
"inputs": {
"entity": "cr5f3_fileupload",
"operation": "create"
}
}
},
"actions": {
"validate_file": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://your-virus-scanner.com/api/scan",
"headers": {
"Content-Type": "application/json",
"Authorization": "@parameters('scannerApiKey')"
},
"body": {
"fileId": "@triggerOutputs()?['body/cr5f3_fileuploadid']",
"fileName": "@triggerOutputs()?['body/cr5f3_filename']",
"fileUrl": "@triggerOutputs()?['body/cr5f3_fileurl']"
}
}
},
"update_validation_status": {
"type": "PowerPlatform",
"inputs": {
"entityName": "cr5f3_fileupload",
"recordId": "@triggerOutputs()?['body/cr5f3_fileuploadid']",
"item": {
"cr5f3_status": "@if(equals(outputs('validate_file')?['body/status'], 'clean'), 2, 4)",
"cr5f3_validationdate": "@utcNow()",
"cr5f3_validationresult": "@outputs('validate_file')?['body/result']"
}
}
},
"process_document": {
"type": "Condition",
"expression": {
"and": [
{
"equals": [
"@outputs('validate_file')?['body/status']",
"clean"
]
},
{
"contains": [
"@triggerOutputs()?['body/cr5f3_filetype']",
"pdf"
]
}
]
},
"actions": {
"extract_document_text": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://your-ocr-service.com/api/extract",
"body": {
"fileUrl": "@triggerOutputs()?['body/cr5f3_fileurl']"
}
}
},
"update_extracted_text": {
"type": "PowerPlatform",
"inputs": {
"entityName": "cr5f3_fileupload",
"recordId": "@triggerOutputs()?['body/cr5f3_fileuploadid']",
"item": {
"cr5f3_extractedtext": "@outputs('extract_document_text')?['body/text']"
}
}
}
}
},
"send_notification": {
"type": "Office365Outlook",
"inputs": {
"method": "POST",
"path": "/v2/Mail",
"body": {
"To": "@triggerOutputs()?['body/_cr5f3_uploadedby_value@OData.Community.Display.V1.FormattedValue']",
"Subject": "File Upload Status: @{triggerOutputs()?['body/cr5f3_filename']}",
"Body": "@{if(equals(outputs('validate_file')?['body/status'], 'clean'), 'Your file has been successfully uploaded and validated.', 'Your file upload was rejected due to security concerns.')}"
}
}
}
}
}
}
Practical Example: Document Management Portal
Complete Document Portal Implementation:
/* Document Management Portal Styling */
.file-management-dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.dashboard-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.view-toggle {
display: flex;
border: 1px solid #e9ecef;
border-radius: 6px;
overflow: hidden;
}
.view-toggle button {
border: none;
border-radius: 0;
padding: 0.5rem 1rem;
}
.view-toggle button.active {
background: var(--primary-color);
color: white;
}
.dashboard-filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-group label {
font-weight: 600;
font-size: 0.875rem;
color: #495057;
}
.filter-select, .search-input {
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 0.875rem;
}
.search-group {
display: flex;
gap: 0.5rem;
}
.search-btn {
padding: 0.5rem 1rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.file-card {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s ease;
cursor: pointer;
}
.file-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: var(--primary-color);
}
.file-card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.file-type-icon {
font-size: 2rem;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 8px;
}
.file-info h4 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
font-weight: 600;
color: #212529;
word-break: break-word;
}
.file-meta {
font-size: 0.75rem;
color: #6c757d;
}
.file-status {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
margin-bottom: 1rem;
}
.file-status.approved {
background: #d4edda;
color: #155724;
}
.file-status.pending {
background: #fff3cd;
color: #856404;
}
.file-status.rejected {
background: #f8d7da;
color: #721c24;
}
.file-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.file-actions button {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border: 1px solid #e9ecef;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.file-actions button:hover {
background: #f8f9fa;
border-color: var(--primary-color);
}
.file-actions .btn-primary {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e9ecef;
}
.pagination {
display: flex;
gap: 0.25rem;
}
.pagination button {
padding: 0.5rem 0.75rem;
border: 1px solid #e9ecef;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination button:hover {
background: #f8f9fa;
border-color: var(--primary-color);
}
.pagination button.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Upload Zone Styling */
.upload-zone {
border: 2px dashed #ced4da;
border-radius: 8px;
padding: 3rem 2rem;
text-align: center;
background: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-zone:hover,
.upload-zone.drag-over {
border-color: var(--primary-color);
background: #e7f3ff;
}
.upload-zone-content {
max-width: 400px;
margin: 0 auto;
}
.upload-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.7;
}
.upload-zone h3 {
margin: 0 0 0.5rem 0;
color: #495057;
}
.upload-zone p {
margin: 0 0 1rem 0;
color: #6c757d;
}
.btn-link {
background: none;
border: none;
color: var(--primary-color);
text-decoration: underline;
cursor: pointer;
font-size: inherit;
}
.upload-restrictions {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.upload-restrictions small {
color: #6c757d;
font-size: 0.75rem;
}
/* File Preview Styling */
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border: 1px solid #e9ecef;
border-radius: 6px;
margin-bottom: 0.75rem;
background: white;
}
.file-info {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.file-icon {
font-size: 1.5rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 6px;
}
.file-name {
font-weight: 600;
color: #212529;
margin-bottom: 0.25rem;
}
.file-meta {
font-size: 0.75rem;
color: #6c757d;
}
.file-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.file-fields {
display: flex;
gap: 0.5rem;
}
.file-category {
min-width: 120px;
padding: 0.25rem 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 0.75rem;
}
.file-description {
min-width: 150px;
padding: 0.25rem 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 0.75rem;
}
.btn-remove {
width: 30px;
height: 30px;
border: 1px solid #dc3545;
background: white;
color: #dc3545;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.btn-remove:hover {
background: #dc3545;
color: white;
}
Troubleshooting Common Issues
File Upload Debugging Checklist:
- File Size Limits: Check both client-side and server-side limits
- File Type Validation: Ensure MIME type checking is consistent
- Security Scanning: Verify virus scanning integration is working
- Storage Permissions: Check Dataverse entity permissions and Azure storage access
- Network Timeouts: Implement proper timeout handling for large files
- Progress Tracking: Ensure upload progress feedback is accurate
- Error Handling: Provide clear error messages for common failure scenarios
Key Takeaways
File upload and management requires careful attention to security, user experience, and performance. Focus on proper validation, clear feedback, and robust error handling to create professional file handling capabilities that users trust and enjoy using.
In the next episode, you’ll learn about advanced Portal Management techniques including user management, role-based access control, and portal administration best practices.