generated from corrad-software/corrad-af-2024

The navigation pane now matches the document structure from database, but breadcrumbs bar became broken, not yet fixed. Added check to folder creation so that subfolders cannot be created in parent folders that don't exist.
2792 lines
89 KiB
Vue
2792 lines
89 KiB
Vue
<script setup>
|
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
// Router
|
|
const router = useRouter();
|
|
|
|
// Define page metadata
|
|
definePageMeta({
|
|
title: "Document Management System",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{
|
|
name: "DMS",
|
|
path: "/dms",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Import new dialog components
|
|
import UploadWithMetadataModal from '~/components/dms/dialogs/UploadWithMetadataModal.vue';
|
|
import CreateNewDialog from '~/components/dms/dialogs/CreateNewDialog.vue';
|
|
import ContextMenu from '~/components/dms/ui/ContextMenu.vue';
|
|
import RsButton from '~/components/RsButton.vue';
|
|
import DocumentPreviewDialog from '~/components/dms/preview/DocumentPreviewDialog.vue';
|
|
|
|
// Core state management
|
|
const activeTab = ref('all');
|
|
const isLoading = ref(true);
|
|
const hasError = ref(false);
|
|
const errorMessage = ref('');
|
|
const componentsLoaded = ref(false);
|
|
const isMobileView = ref(false);
|
|
|
|
// Navigation state (Windows Explorer style)
|
|
const currentPath = ref('/');
|
|
const navigationHistory = ref(['/']);
|
|
const historyIndex = ref(0);
|
|
const breadcrumbPath = ref([]);
|
|
const selectedItem = ref(null);
|
|
const selectedItems = ref(new Set());
|
|
|
|
// View state
|
|
const viewMode = ref('list'); // 'list' | 'grid' | 'compact'
|
|
const sortBy = ref('name');
|
|
const sortOrder = ref('asc');
|
|
const showPreviewPanel = ref(true);
|
|
const previewContent = ref(null);
|
|
|
|
// Search and filter state
|
|
const searchQuery = ref('');
|
|
const searchSuggestions = ref([]);
|
|
const showSearchSuggestions = ref(false);
|
|
const recentSearches = ref(['Budget 2024', 'Annual Report', 'Project Files']);
|
|
|
|
// Tree view state for collapse/expand
|
|
const expandedItems = ref(new Set(['cabinet-1', 'cabinet-2'])); // Start with cabinets expanded
|
|
|
|
// Toggle expand/collapse for tree items
|
|
const toggleItemExpanded = (itemId, event) => {
|
|
if (event) {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
if (expandedItems.value.has(itemId)) {
|
|
expandedItems.value.delete(itemId);
|
|
} else {
|
|
expandedItems.value.add(itemId);
|
|
}
|
|
};
|
|
|
|
// Check if item is expanded
|
|
const isItemExpanded = (itemId) => {
|
|
return expandedItems.value.has(itemId);
|
|
};
|
|
|
|
// Dialog states
|
|
const showUploadModal = ref(false);
|
|
const showCreateNewDialog = ref(false);
|
|
const showContextMenu = ref(false);
|
|
const contextMenuPosition = ref({ x: 0, y: 0 });
|
|
const contextMenuTarget = ref(null);
|
|
|
|
// Document preview dialog state
|
|
const showDocumentPreview = ref(false);
|
|
const previewDocument = ref(null);
|
|
const previewMode = ref('view');
|
|
|
|
// Hover thumbnail state
|
|
const showHoverThumbnail = ref(false);
|
|
const hoverThumbnailDocument = ref(null);
|
|
const hoverThumbnailPosition = ref({ x: 0, y: 0 });
|
|
const thumbnailHoverTimeout = ref(null);
|
|
|
|
// Drag and drop state
|
|
const isDragging = ref(false);
|
|
const draggedItems = ref([]);
|
|
const dropTarget = ref(null);
|
|
|
|
// Enhanced hierarchical structure (Cabinet > Drawer > Folder structure)
|
|
const documentStructure = ref([
|
|
// {
|
|
// id: 'cabinet-1',
|
|
// name: 'JKR Document Cabinet',
|
|
// type: 'cabinet',
|
|
// path: '/jkr-cabinet',
|
|
// accessLevel: 'department',
|
|
// itemCount: 234,
|
|
// children: [
|
|
// {
|
|
// id: 'drawer-1-1',
|
|
// name: 'Administrative Drawer',
|
|
// type: 'drawer',
|
|
// path: '/jkr-cabinet/administrative',
|
|
// accessLevel: 'department',
|
|
// itemCount: 89,
|
|
// children: [
|
|
// {
|
|
// id: 'folder-1-1-1',
|
|
// name: 'Personnel Files',
|
|
// type: 'folder',
|
|
// path: '/jkr-cabinet/administrative/personnel',
|
|
// accessLevel: 'private',
|
|
// itemCount: 45,
|
|
// children: []
|
|
// },
|
|
// {
|
|
// id: 'folder-1-1-2',
|
|
// name: 'Budget Reports',
|
|
// type: 'folder',
|
|
// path: '/jkr-cabinet/administrative/budget',
|
|
// accessLevel: 'department',
|
|
// itemCount: 23,
|
|
// children: []
|
|
// }
|
|
// ]
|
|
// },
|
|
// {
|
|
// id: 'drawer-1-2',
|
|
// name: 'Project Drawer',
|
|
// type: 'drawer',
|
|
// path: '/jkr-cabinet/projects',
|
|
// accessLevel: 'department',
|
|
// itemCount: 145,
|
|
// children: [
|
|
// {
|
|
// id: 'folder-1-2-1',
|
|
// name: 'Highway Projects',
|
|
// type: 'folder',
|
|
// path: '/jkr-cabinet/projects/highway',
|
|
// accessLevel: 'department',
|
|
// itemCount: 67,
|
|
// children: []
|
|
// },
|
|
// {
|
|
// id: 'folder-1-2-2',
|
|
// name: 'Building Projects',
|
|
// type: 'folder',
|
|
// path: '/jkr-cabinet/projects/building',
|
|
// accessLevel: 'department',
|
|
// itemCount: 78,
|
|
// children: []
|
|
// }
|
|
// ]
|
|
// }
|
|
// ]
|
|
// },
|
|
// {
|
|
// id: 'cabinet-2',
|
|
// name: 'Public Documents Cabinet',
|
|
// type: 'cabinet',
|
|
// path: '/public-cabinet',
|
|
// accessLevel: 'public',
|
|
// itemCount: 156,
|
|
// children: [
|
|
// {
|
|
// id: 'drawer-2-1',
|
|
// name: 'Forms & Applications',
|
|
// type: 'drawer',
|
|
// path: '/public-cabinet/forms',
|
|
// accessLevel: 'public',
|
|
// itemCount: 89,
|
|
// children: []
|
|
// },
|
|
// {
|
|
// id: 'drawer-2-2',
|
|
// name: 'Public Announcements',
|
|
// type: 'drawer',
|
|
// path: '/public-cabinet/announcements',
|
|
// accessLevel: 'public',
|
|
// itemCount: 67,
|
|
// children: []
|
|
// }
|
|
// ]
|
|
// },
|
|
// {
|
|
// id: 'cabinet-3',
|
|
// name: 'Personal Documents',
|
|
// type: 'cabinet',
|
|
// path: '/personal-cabinet',
|
|
// accessLevel: 'personal',
|
|
// itemCount: 45,
|
|
// children: [
|
|
// {
|
|
// id: 'drawer-3-1',
|
|
// name: 'My Documents',
|
|
// type: 'drawer',
|
|
// path: '/personal-cabinet/my-docs',
|
|
// accessLevel: 'personal',
|
|
// itemCount: 25,
|
|
// children: []
|
|
// },
|
|
// {
|
|
// id: 'drawer-3-2',
|
|
// name: 'Private Files',
|
|
// type: 'drawer',
|
|
// path: '/personal-cabinet/private',
|
|
// accessLevel: 'private',
|
|
// itemCount: 20,
|
|
// children: []
|
|
// }
|
|
// ]
|
|
// }
|
|
]);
|
|
|
|
// Current folder contents (what's displayed in the main view)
|
|
const currentFolderContents = ref([]);
|
|
|
|
// Undo/Redo system
|
|
const operationHistory = ref([]);
|
|
const historyPointer = ref(-1);
|
|
|
|
// Enhanced tab definitions
|
|
const tabs = [
|
|
{
|
|
id: 'all',
|
|
label: 'All Documents',
|
|
icon: 'mdi:folder-multiple',
|
|
color: 'blue',
|
|
description: 'All accessible documents',
|
|
count: computed(() => getTotalItemCount())
|
|
},
|
|
{
|
|
id: 'public',
|
|
label: 'Public',
|
|
icon: 'mdi:earth',
|
|
color: 'green',
|
|
description: 'Publicly accessible documents',
|
|
count: computed(() => getFilteredItemCount('public'))
|
|
},
|
|
{
|
|
id: 'private',
|
|
label: 'Private',
|
|
icon: 'mdi:lock',
|
|
color: 'red',
|
|
description: 'Private access only',
|
|
count: computed(() => getFilteredItemCount('private'))
|
|
},
|
|
{
|
|
id: 'personal',
|
|
label: 'Personal',
|
|
icon: 'mdi:account',
|
|
color: 'purple',
|
|
description: 'Your personal documents',
|
|
count: computed(() => getFilteredItemCount('personal'))
|
|
}
|
|
];
|
|
|
|
// File type handling (Windows Explorer style)
|
|
const getFileTypeIcon = (fileName) => {
|
|
if (!fileName) return 'mdi:file-document';
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
const iconMap = {
|
|
pdf: 'mdi:file-pdf-box',
|
|
doc: 'mdi:file-word-box',
|
|
docx: 'mdi:file-word-box',
|
|
xls: 'mdi:file-excel-box',
|
|
xlsx: 'mdi:file-excel-box',
|
|
ppt: 'mdi:file-powerpoint-box',
|
|
pptx: 'mdi:file-powerpoint-box',
|
|
txt: 'mdi:file-document-outline',
|
|
jpg: 'mdi:file-image',
|
|
jpeg: 'mdi:file-image',
|
|
png: 'mdi:file-image',
|
|
gif: 'mdi:file-image',
|
|
zip: 'mdi:zip-box',
|
|
rar: 'mdi:archive',
|
|
mp4: 'mdi:file-video',
|
|
avi: 'mdi:file-video',
|
|
mp3: 'mdi:file-music',
|
|
wav: 'mdi:file-music',
|
|
default: 'mdi:file-document'
|
|
};
|
|
return iconMap[extension] || iconMap.default;
|
|
};
|
|
|
|
const getFileTypeColor = (fileName) => {
|
|
if (!fileName) return 'text-gray-500 dark:text-gray-400';
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
const colorMap = {
|
|
pdf: 'text-red-600 dark:text-red-400',
|
|
doc: 'text-blue-600 dark:text-blue-400',
|
|
docx: 'text-blue-600 dark:text-blue-400',
|
|
xls: 'text-green-600 dark:text-green-400',
|
|
xlsx: 'text-green-600 dark:text-green-400',
|
|
ppt: 'text-orange-600 dark:text-orange-400',
|
|
pptx: 'text-orange-600 dark:text-orange-400',
|
|
txt: 'text-gray-600 dark:text-gray-400',
|
|
jpg: 'text-purple-600 dark:text-purple-400',
|
|
jpeg: 'text-purple-600 dark:text-purple-400',
|
|
png: 'text-purple-600 dark:text-purple-400',
|
|
gif: 'text-purple-600 dark:text-purple-400',
|
|
zip: 'text-yellow-600 dark:text-yellow-400',
|
|
rar: 'text-yellow-600 dark:text-yellow-400',
|
|
mp4: 'text-indigo-600 dark:text-indigo-400',
|
|
avi: 'text-indigo-600 dark:text-indigo-400',
|
|
mp3: 'text-pink-600 dark:text-pink-400',
|
|
wav: 'text-pink-600 dark:text-pink-400',
|
|
default: 'text-gray-500 dark:text-gray-400'
|
|
};
|
|
return colorMap[extension] || colorMap.default;
|
|
};
|
|
|
|
// Function to build path from parent_id chain
|
|
const buildPathFromParentId = (item, allItems) => {
|
|
const pathSegments = [];
|
|
let currentItem = item;
|
|
|
|
// Traverse up the parent chain until we reach a root item (null parent_id)
|
|
while (currentItem) {
|
|
// Add the current item's name to the start of the path
|
|
pathSegments.unshift(currentItem.name.toLowerCase().replace(/\s+/g, '-'));
|
|
|
|
// If we've reached a root item (null parent_id), break the loop
|
|
if (!currentItem.parent_id) break;
|
|
|
|
// Find the parent item
|
|
currentItem = allItems.find(i => i.id === currentItem.parent_id);
|
|
|
|
// Break if we can't find the parent (shouldn't happen in a valid tree)
|
|
if (!currentItem) break;
|
|
}
|
|
|
|
// Construct the final path with leading slash
|
|
return '/' + pathSegments.join('/');
|
|
};
|
|
|
|
// Function to convert database records to frontend structure
|
|
const convertDbToFrontendStructure = (dbRecords) => {
|
|
// First pass: Create a map of all items with their paths
|
|
const itemsWithPaths = dbRecords.folders.map(record => ({
|
|
id: record.cb_id,
|
|
name: record.cb_name,
|
|
type: record.cb_type || 'folder',
|
|
parent_id: record.cb_parent_id,
|
|
accessLevel: record.cb_access_level || 'public',
|
|
itemCount: record.item_count || 0,
|
|
children: [],
|
|
path: '' // Will be populated in the next step
|
|
}));
|
|
|
|
// Second pass: Build paths for all items
|
|
itemsWithPaths.forEach(item => {
|
|
item.path = buildPathFromParentId(item, itemsWithPaths);
|
|
});
|
|
|
|
// Third pass: Build the tree structure
|
|
const rootItems = itemsWithPaths.filter(item => !item.parent_id);
|
|
|
|
// Recursive function to build the tree
|
|
const buildTree = (items) => {
|
|
return items.map(item => {
|
|
const children = itemsWithPaths.filter(child => child.parent_id === item.id);
|
|
return {
|
|
...item,
|
|
children: children.length > 0 ? buildTree(children) : []
|
|
};
|
|
});
|
|
};
|
|
|
|
return buildTree(rootItems);
|
|
};
|
|
|
|
const mapAPIToDocumentStructure = async () => {
|
|
try {
|
|
const response = await fetch('/api/dms/folder');
|
|
const dbRecords = await response.json();
|
|
|
|
console.log(dbRecords);
|
|
|
|
// Convert database records to frontend structure
|
|
documentStructure.value = convertDbToFrontendStructure(dbRecords);
|
|
|
|
return documentStructure.value;
|
|
} catch (error) {
|
|
console.error('Error mapping API data:', error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
// Navigation functions
|
|
const buildBreadcrumbs = async (path) => {
|
|
if (path === '/') {
|
|
return [{ name: 'Root', path: '/', type: 'root' }];
|
|
}
|
|
|
|
const crumbs = [{ name: 'Root', path: '/', type: 'root' }];
|
|
const segments = path.split('/').filter(Boolean);
|
|
|
|
documentStructure.value = await mapAPIToDocumentStructure();
|
|
console.log(documentStructure.value);
|
|
let itemsToSearch = documentStructure.value;
|
|
console.log(itemsToSearch);
|
|
let currentPath = '';
|
|
|
|
for (const segment of segments) {
|
|
currentPath += '/' + segment;
|
|
const isApiStructure = itemsToSearch.length > 0 && 'cb_id' in itemsToSearch[0];
|
|
|
|
// Slugify function to match path segments with names
|
|
const slugify = (text) => text.toString().toLowerCase()
|
|
.replace(/\s+/g, '-')
|
|
.replace(/[^\w-]+/g, '');
|
|
|
|
const foundItem = itemsToSearch.find(item => {
|
|
const itemName = isApiStructure ? item.cabinet_name : item.name;
|
|
if (!itemName) return false;
|
|
|
|
if (item.path) {
|
|
// Prefer matching with existing path property
|
|
return item.path === currentPath;
|
|
}
|
|
// Fallback to comparing slugified names
|
|
return slugify(itemName) === segment;
|
|
});
|
|
|
|
if (foundItem) {
|
|
const itemName = isApiStructure ? foundItem.cabinet_name : foundItem.name;
|
|
const itemPath = foundItem.path || currentPath;
|
|
const itemType = foundItem.type || 'folder';
|
|
crumbs.push({
|
|
name: itemName,
|
|
path: itemPath,
|
|
type: itemType
|
|
});
|
|
itemsToSearch = foundItem.children || [];
|
|
} else {
|
|
// If an item is not found in the structure, use the segment as its name
|
|
// This can happen for paths pointing to files or during transitions
|
|
crumbs.push({
|
|
name: segment,
|
|
path: currentPath,
|
|
type: 'folder'
|
|
});
|
|
itemsToSearch = []; // Stop searching deeper
|
|
}
|
|
}
|
|
|
|
return crumbs;
|
|
};
|
|
|
|
const navigateToPath = (path, addToHistory = true) => {
|
|
if (addToHistory && path !== currentPath.value) {
|
|
addToNavigationHistory(path);
|
|
}
|
|
|
|
currentPath.value = path;
|
|
breadcrumbPath.value = buildBreadcrumbs(path);
|
|
loadFolderContents(path);
|
|
selectedItem.value = null;
|
|
selectedItems.value.clear();
|
|
};
|
|
|
|
const addToNavigationHistory = (path) => {
|
|
// Remove any history after current index
|
|
navigationHistory.value = navigationHistory.value.slice(0, historyIndex.value + 1);
|
|
navigationHistory.value.push(path);
|
|
historyIndex.value = navigationHistory.value.length - 1;
|
|
|
|
// Limit history size
|
|
if (navigationHistory.value.length > 50) {
|
|
navigationHistory.value.shift();
|
|
historyIndex.value--;
|
|
}
|
|
};
|
|
|
|
const canGoBack = computed(() => historyIndex.value > 0);
|
|
const canGoForward = computed(() => historyIndex.value < navigationHistory.value.length - 1);
|
|
|
|
const goBack = () => {
|
|
if (canGoBack.value) {
|
|
historyIndex.value--;
|
|
navigateToPath(navigationHistory.value[historyIndex.value], false);
|
|
}
|
|
};
|
|
|
|
const goForward = () => {
|
|
if (canGoForward.value) {
|
|
historyIndex.value++;
|
|
navigateToPath(navigationHistory.value[historyIndex.value], false);
|
|
}
|
|
};
|
|
|
|
const goUp = () => {
|
|
const segments = currentPath.value.split('/').filter(Boolean);
|
|
if (segments.length > 0) {
|
|
segments.pop();
|
|
const parentPath = segments.length > 0 ? '/' + segments.join('/') : '/';
|
|
navigateToPath(parentPath);
|
|
}
|
|
};
|
|
|
|
// Content loading and management
|
|
const loadFolderContents = async (path) => {
|
|
isLoading.value = true;
|
|
|
|
try {
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
const item = findItemByPath(path, documentStructure.value);
|
|
if (item && item.children) {
|
|
currentFolderContents.value = item.children.map(child => ({
|
|
...child,
|
|
lastModified: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
|
|
size: generateFileSize(child.type),
|
|
owner: 'System Admin'
|
|
}));
|
|
} else {
|
|
// Mock files for leaf folders
|
|
currentFolderContents.value = generateMockFiles(path);
|
|
}
|
|
|
|
// Apply current filter
|
|
if (activeTab.value !== 'all') {
|
|
currentFolderContents.value = currentFolderContents.value.filter(item =>
|
|
item.accessLevel === activeTab.value ||
|
|
(item.tags && item.tags.some(tag => tag.toLowerCase() === activeTab.value))
|
|
);
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Failed to load folder contents:', err);
|
|
hasError.value = true;
|
|
errorMessage.value = 'Failed to load folder contents';
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const findItemByPath = (path, items = documentStructure.value) => {
|
|
// Check if we're using the API-based structure or mock structure
|
|
const isApiStructure = items.length > 0 && 'cb_id' in items[0];
|
|
|
|
for (const item of items) {
|
|
const itemPath = isApiStructure ? (item.path || ('/' + (item.cabinet_name || '').toString().toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, ''))) : item.path;
|
|
|
|
if (itemPath === path) {
|
|
return item;
|
|
}
|
|
|
|
// Check children if they exist and path might be inside this subtree
|
|
if (item.children && path.startsWith(itemPath + (itemPath === '/' ? '' : '/'))) {
|
|
const found = findItemByPath(path, item.children);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const generateMockFiles = (path) => {
|
|
const files = [
|
|
{
|
|
id: 'file-1',
|
|
name: 'Annual Report 2024.pdf',
|
|
type: 'file',
|
|
accessLevel: 'department',
|
|
tags: ['Annual', 'Report'],
|
|
description: 'Comprehensive annual report for 2024'
|
|
},
|
|
{
|
|
id: 'file-2',
|
|
name: 'Budget Analysis.xlsx',
|
|
type: 'file',
|
|
accessLevel: 'private',
|
|
tags: ['Budget', 'Financial'],
|
|
description: 'Detailed budget analysis spreadsheet'
|
|
},
|
|
{
|
|
id: 'file-3',
|
|
name: 'Project Proposal.docx',
|
|
type: 'file',
|
|
accessLevel: 'public',
|
|
tags: ['Project', 'Proposal'],
|
|
description: 'New project proposal document'
|
|
}
|
|
];
|
|
|
|
return files.map(file => ({
|
|
...file,
|
|
path: `${path}/${file.name}`,
|
|
lastModified: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
|
|
size: generateFileSize('file'),
|
|
owner: 'System Admin'
|
|
}));
|
|
};
|
|
|
|
const generateFileSize = (type) => {
|
|
if (type === 'file') {
|
|
const sizes = ['1.2 MB', '3.4 MB', '567 KB', '2.1 MB', '890 KB'];
|
|
return sizes[Math.floor(Math.random() * sizes.length)];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Item count functions
|
|
const getTotalItemCount = () => {
|
|
const countItems = (items) => {
|
|
let count = 0;
|
|
for (const item of items) {
|
|
count++;
|
|
if (item.children) {
|
|
count += countItems(item.children);
|
|
}
|
|
}
|
|
return count;
|
|
};
|
|
return countItems(documentStructure.value);
|
|
};
|
|
|
|
const getFilteredItemCount = (filter) => {
|
|
const countFilteredItems = (items) => {
|
|
let count = 0;
|
|
for (const item of items) {
|
|
if (item.accessLevel === filter ||
|
|
(item.tags && item.tags.some(tag => tag.toLowerCase() === filter))) {
|
|
count++;
|
|
}
|
|
if (item.children) {
|
|
count += countFilteredItems(item.children);
|
|
}
|
|
}
|
|
return count;
|
|
};
|
|
return countFilteredItems(documentStructure.value);
|
|
};
|
|
|
|
// All the other functions that we need for the template
|
|
const selectItem = (item) => {
|
|
selectedItem.value = item;
|
|
if (item.type === 'file') {
|
|
loadPreview(item);
|
|
} else {
|
|
previewContent.value = null;
|
|
}
|
|
};
|
|
|
|
const toggleItemSelection = (item) => {
|
|
if (selectedItems.value.has(item.id)) {
|
|
selectedItems.value.delete(item.id);
|
|
} else {
|
|
selectedItems.value.add(item.id);
|
|
}
|
|
};
|
|
|
|
const selectAllItems = () => {
|
|
currentFolderContents.value.forEach(item => {
|
|
selectedItems.value.add(item.id);
|
|
});
|
|
};
|
|
|
|
const clearSelection = () => {
|
|
selectedItems.value.clear();
|
|
selectedItem.value = null;
|
|
previewContent.value = null;
|
|
};
|
|
|
|
const loadPreview = async (item) => {
|
|
if (item.type !== 'file') return;
|
|
|
|
try {
|
|
const extension = item.name.split('.').pop().toLowerCase();
|
|
|
|
if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) {
|
|
previewContent.value = {
|
|
type: 'image',
|
|
url: `/api/files/${item.id}/preview`,
|
|
metadata: {
|
|
dimensions: '1920x1080',
|
|
fileSize: item.size,
|
|
colorSpace: 'RGB'
|
|
}
|
|
};
|
|
} else if (extension === 'pdf') {
|
|
previewContent.value = {
|
|
type: 'pdf',
|
|
url: `/api/files/${item.id}/preview`,
|
|
metadata: {
|
|
pages: 12,
|
|
fileSize: item.size,
|
|
version: '1.4'
|
|
}
|
|
};
|
|
} else if (['txt', 'md'].includes(extension)) {
|
|
previewContent.value = {
|
|
type: 'text',
|
|
content: 'Sample text content...',
|
|
metadata: {
|
|
lines: 150,
|
|
words: 1250,
|
|
characters: 7500
|
|
}
|
|
};
|
|
} else {
|
|
previewContent.value = {
|
|
type: 'metadata',
|
|
metadata: {
|
|
fileSize: item.size,
|
|
lastModified: item.lastModified,
|
|
owner: item.owner,
|
|
tags: item.tags || []
|
|
}
|
|
};
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load preview:', err);
|
|
}
|
|
};
|
|
|
|
const handleItemDoubleClick = (item) => {
|
|
if (item.type === 'file') {
|
|
openDocumentPreview(item);
|
|
} else {
|
|
navigateToPath(item.path);
|
|
}
|
|
};
|
|
|
|
const changeTab = (tabId) => {
|
|
activeTab.value = tabId;
|
|
loadFolderContents(currentPath.value);
|
|
};
|
|
|
|
const handleDragStart = (event, item) => {
|
|
isDragging.value = true;
|
|
draggedItems.value = selectedItems.value.has(item.id)
|
|
? Array.from(selectedItems.value).map(id => currentFolderContents.value.find(i => i.id === id))
|
|
: [item];
|
|
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
event.dataTransfer.setData('text/plain', JSON.stringify(draggedItems.value));
|
|
};
|
|
|
|
const handleDragOver = (event, item) => {
|
|
if (item.type === 'folder' || item.type === 'drawer' || item.type === 'cabinet') {
|
|
event.preventDefault();
|
|
dropTarget.value = item;
|
|
}
|
|
};
|
|
|
|
const handleDragLeave = () => {
|
|
dropTarget.value = null;
|
|
};
|
|
|
|
const handleDrop = (event, targetItem) => {
|
|
event.preventDefault();
|
|
|
|
if (targetItem.type === 'folder' || targetItem.type === 'drawer' || targetItem.type === 'cabinet') {
|
|
const items = JSON.parse(event.dataTransfer.getData('text/plain'));
|
|
moveItems(items, targetItem);
|
|
}
|
|
|
|
isDragging.value = false;
|
|
dropTarget.value = null;
|
|
draggedItems.value = [];
|
|
};
|
|
|
|
const moveItems = (items, target) => {
|
|
console.log('Moving items:', items, 'to:', target.name);
|
|
addOperation('move', { items, target, from: currentPath.value });
|
|
};
|
|
|
|
const addOperation = (type, data) => {
|
|
const operation = {
|
|
type,
|
|
data,
|
|
timestamp: Date.now(),
|
|
path: currentPath.value
|
|
};
|
|
|
|
operationHistory.value = operationHistory.value.slice(0, historyPointer.value + 1);
|
|
operationHistory.value.push(operation);
|
|
historyPointer.value = operationHistory.value.length - 1;
|
|
|
|
if (operationHistory.value.length > 100) {
|
|
operationHistory.value.shift();
|
|
historyPointer.value--;
|
|
}
|
|
};
|
|
|
|
const canUndo = computed(() => historyPointer.value >= 0);
|
|
const canRedo = computed(() => historyPointer.value < operationHistory.value.length - 1);
|
|
|
|
const undo = () => {
|
|
if (canUndo.value) {
|
|
const operation = operationHistory.value[historyPointer.value];
|
|
performUndo(operation);
|
|
historyPointer.value--;
|
|
}
|
|
};
|
|
|
|
const redo = () => {
|
|
if (canRedo.value) {
|
|
historyPointer.value++;
|
|
const operation = operationHistory.value[historyPointer.value];
|
|
performRedo(operation);
|
|
}
|
|
};
|
|
|
|
const performUndo = (operation) => {
|
|
console.log('Undoing:', operation.type);
|
|
loadFolderContents(currentPath.value);
|
|
};
|
|
|
|
const performRedo = (operation) => {
|
|
console.log('Redoing:', operation.type);
|
|
loadFolderContents(currentPath.value);
|
|
};
|
|
|
|
const handleKeyboardShortcuts = (event) => {
|
|
if (event.altKey && event.key === 'ArrowLeft') {
|
|
event.preventDefault();
|
|
goBack();
|
|
} else if (event.altKey && event.key === 'ArrowRight') {
|
|
event.preventDefault();
|
|
goForward();
|
|
} else if (event.altKey && event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
goUp();
|
|
} else if (event.ctrlKey && event.key === 'z' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
undo();
|
|
} else if (event.ctrlKey && (event.key === 'y' || (event.key === 'z' && event.shiftKey))) {
|
|
event.preventDefault();
|
|
redo();
|
|
} else if (event.ctrlKey && event.key === 'u') {
|
|
event.preventDefault();
|
|
openUploadModal();
|
|
} else if (event.ctrlKey && event.shiftKey && event.key === 'n') {
|
|
event.preventDefault();
|
|
openCreateNewDialog();
|
|
} else if (event.ctrlKey && event.key === 'a') {
|
|
event.preventDefault();
|
|
selectAllItems();
|
|
} else if (event.key === 'F2' && selectedItem.value) {
|
|
event.preventDefault();
|
|
renameItem(selectedItem.value);
|
|
} else if (event.key === 'Delete' && selectedItems.value.size > 0) {
|
|
event.preventDefault();
|
|
deleteSelectedItems();
|
|
} else if (event.key === 'Enter' && selectedItem.value) {
|
|
event.preventDefault();
|
|
handleItemDoubleClick(selectedItem.value);
|
|
} else if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
clearSelection();
|
|
closeAllDialogs();
|
|
}
|
|
};
|
|
|
|
const handleContextMenuAction = ({ item, target }) => {
|
|
switch (item.id) {
|
|
case 'open':
|
|
handleItemDoubleClick(target);
|
|
break;
|
|
case 'rename':
|
|
renameItem(target);
|
|
break;
|
|
case 'delete':
|
|
deleteItem(target);
|
|
break;
|
|
case 'copy':
|
|
copyItems([target]);
|
|
break;
|
|
case 'cut':
|
|
cutItems([target]);
|
|
break;
|
|
case 'paste':
|
|
pasteItems();
|
|
break;
|
|
case 'properties':
|
|
showProperties(target);
|
|
break;
|
|
default:
|
|
console.log('Unhandled context menu action:', item.id);
|
|
}
|
|
};
|
|
|
|
const renameItem = (item) => {
|
|
const newName = prompt('Enter new name:', item.name);
|
|
if (newName && newName !== item.name) {
|
|
addOperation('rename', { item, oldName: item.name, newName });
|
|
item.name = newName;
|
|
console.log('Renamed item to:', newName);
|
|
}
|
|
};
|
|
|
|
const deleteItem = (item) => {
|
|
if (confirm(`Delete "${item.name}"?`)) {
|
|
addOperation('delete', { item, path: currentPath.value });
|
|
console.log('Deleted item:', item.name);
|
|
loadFolderContents(currentPath.value);
|
|
}
|
|
};
|
|
|
|
const deleteSelectedItems = () => {
|
|
const items = Array.from(selectedItems.value).map(id =>
|
|
currentFolderContents.value.find(item => item.id === id)
|
|
);
|
|
|
|
if (confirm(`Delete ${items.length} selected item(s)?`)) {
|
|
addOperation('delete', { items, path: currentPath.value });
|
|
console.log('Deleted items:', items.map(i => i.name));
|
|
clearSelection();
|
|
loadFolderContents(currentPath.value);
|
|
}
|
|
};
|
|
|
|
const copyItems = (items) => {
|
|
console.log('Copied items:', items.map(i => i.name));
|
|
};
|
|
|
|
const cutItems = (items) => {
|
|
console.log('Cut items:', items.map(i => i.name));
|
|
};
|
|
|
|
const pasteItems = () => {
|
|
console.log('Pasted items to:', currentPath.value);
|
|
};
|
|
|
|
const showProperties = (item) => {
|
|
console.log('Show properties for:', item.name);
|
|
};
|
|
|
|
const openUploadModal = () => {
|
|
showUploadModal.value = true;
|
|
};
|
|
|
|
const openCreateNewDialog = () => {
|
|
showCreateNewDialog.value = true;
|
|
};
|
|
|
|
const closeAllDialogs = () => {
|
|
showUploadModal.value = false;
|
|
showCreateNewDialog.value = false;
|
|
showContextMenu.value = false;
|
|
};
|
|
|
|
// Global click handler to close context menu
|
|
const handleGlobalClick = (event) => {
|
|
if (showContextMenu.value) {
|
|
showContextMenu.value = false;
|
|
}
|
|
};
|
|
|
|
// Global right-click handler to close context menu on background
|
|
const handleGlobalContextMenu = (event) => {
|
|
// Only handle if it's not on a specific item
|
|
const target = event.target.closest('[data-context-item]');
|
|
if (!target && showContextMenu.value) {
|
|
showContextMenu.value = false;
|
|
}
|
|
};
|
|
|
|
// Upload and create handlers
|
|
const handleUploadComplete = (data) => {
|
|
console.log('Upload completed:', data);
|
|
addOperation('upload', { files: data.files, path: currentPath.value });
|
|
loadFolderContents(currentPath.value);
|
|
};
|
|
|
|
const handleCreateComplete = (data) => {
|
|
console.log('Creation completed:', data);
|
|
addOperation('create', { item: data, path: currentPath.value });
|
|
loadFolderContents(currentPath.value);
|
|
};
|
|
|
|
// Context menu
|
|
const handleDocumentContextMenu = (event, document) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
// Close any existing context menu first
|
|
showContextMenu.value = false;
|
|
|
|
// Use nextTick to ensure the menu is closed before opening new one
|
|
nextTick(() => {
|
|
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
|
|
contextMenuTarget.value = document;
|
|
showContextMenu.value = true;
|
|
});
|
|
};
|
|
|
|
const handleTreeContextMenu = ({ event, item }) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
// Close any existing context menu first
|
|
showContextMenu.value = false;
|
|
|
|
// Use nextTick to ensure the menu is closed before opening new one
|
|
nextTick(() => {
|
|
contextMenuPosition.value = { x: event.clientX, y: event.clientY };
|
|
contextMenuTarget.value = item;
|
|
showContextMenu.value = true;
|
|
});
|
|
};
|
|
|
|
// Utility functions
|
|
const checkMobileView = () => {
|
|
isMobileView.value = window.innerWidth < 768;
|
|
};
|
|
|
|
// Force reactivity update for grid columns on window resize
|
|
const updateGridLayout = () => {
|
|
// Force a re-computation of gridColumns by triggering reactivity
|
|
checkMobileView();
|
|
};
|
|
|
|
const formatFileSize = (size) => {
|
|
if (typeof size === 'string') return size;
|
|
if (!size) return '0 B';
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let index = 0;
|
|
while (size >= 1024 && index < units.length - 1) {
|
|
size /= 1024;
|
|
index++;
|
|
}
|
|
return `${size.toFixed(1)} ${units[index]}`;
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffTime = Math.abs(now - date);
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffDays === 1) return 'Yesterday';
|
|
if (diffDays <= 7) return `${diffDays} days ago`;
|
|
return date.toLocaleDateString();
|
|
};
|
|
|
|
// Tab styling
|
|
const getTabClasses = (tab) => {
|
|
const isActive = activeTab.value === tab.id;
|
|
|
|
if (isActive) {
|
|
switch (tab.color) {
|
|
case 'blue':
|
|
return 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-2 border-blue-300 dark:border-blue-700 shadow-sm';
|
|
case 'green':
|
|
return 'bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-2 border-green-300 dark:border-green-700 shadow-sm';
|
|
case 'red':
|
|
return 'bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300 border-2 border-red-300 dark:border-red-700 shadow-sm';
|
|
case 'purple':
|
|
return 'bg-purple-100 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 border-2 border-purple-300 dark:border-purple-700 shadow-sm';
|
|
default:
|
|
return 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-2 border-gray-300 dark:border-gray-600 shadow-sm';
|
|
}
|
|
} else {
|
|
return 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600';
|
|
}
|
|
};
|
|
|
|
const getTabIconClasses = (tab) => {
|
|
const isActive = activeTab.value === tab.id;
|
|
|
|
if (isActive) {
|
|
switch (tab.color) {
|
|
case 'blue':
|
|
return 'text-blue-600 dark:text-blue-400';
|
|
case 'green':
|
|
return 'text-green-600 dark:text-green-400';
|
|
case 'red':
|
|
return 'text-red-600 dark:text-red-400';
|
|
case 'purple':
|
|
return 'text-purple-600 dark:text-purple-400';
|
|
default:
|
|
return 'text-gray-600 dark:text-gray-400';
|
|
}
|
|
} else {
|
|
return 'text-gray-500 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-400';
|
|
}
|
|
};
|
|
|
|
// Lifecycle hooks
|
|
onMounted(async () => {
|
|
checkMobileView();
|
|
|
|
// Add event listeners
|
|
window.addEventListener('resize', updateGridLayout);
|
|
window.addEventListener('keydown', handleKeyboardShortcuts);
|
|
document.addEventListener('click', handleGlobalClick);
|
|
document.addEventListener('contextmenu', handleGlobalContextMenu);
|
|
|
|
// Populate document structure
|
|
await mapAPIToDocumentStructure();
|
|
|
|
// Initialize navigation
|
|
navigateToPath('/', false);
|
|
|
|
// Set loading to false after initialization
|
|
isLoading.value = false;
|
|
componentsLoaded.value = true;
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', updateGridLayout);
|
|
window.removeEventListener('keydown', handleKeyboardShortcuts);
|
|
document.removeEventListener('click', handleGlobalClick);
|
|
document.removeEventListener('contextmenu', handleGlobalContextMenu);
|
|
});
|
|
|
|
// Utility functions for document status and access level
|
|
const getDocumentStatusClass = (document) => {
|
|
switch (document.status) {
|
|
case 'active':
|
|
return 'opacity-100';
|
|
case 'pending':
|
|
return 'opacity-75 bg-yellow-50 dark:bg-yellow-900/10 border-yellow-200 dark:border-yellow-800';
|
|
case 'restricted':
|
|
return 'opacity-90 bg-red-50 dark:bg-red-900/10 border-red-200 dark:border-red-800';
|
|
default:
|
|
return 'opacity-100';
|
|
}
|
|
};
|
|
|
|
const getAccessLevelBadge = (accessLevel) => {
|
|
switch (accessLevel) {
|
|
case 'public':
|
|
return { class: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400', text: 'Public' };
|
|
case 'department':
|
|
return { class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400', text: 'Department' };
|
|
case 'restricted':
|
|
return { class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400', text: 'Restricted' };
|
|
case 'confidential':
|
|
return { class: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400', text: 'Confidential' };
|
|
default:
|
|
return { class: 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400', text: 'Unknown' };
|
|
}
|
|
};
|
|
|
|
// User permissions (can be made dynamic based on user role)
|
|
const canEdit = ref(true);
|
|
const canDelete = ref(true);
|
|
const canShare = ref(true);
|
|
|
|
// Grid columns computation based on preview panel state and screen size
|
|
const gridColumns = computed(() => {
|
|
// Base columns when preview panel is closed
|
|
const baseColumns = {
|
|
sm: 2, // mobile
|
|
md: 4, // tablet
|
|
lg: 6, // laptop
|
|
xl: 8, // desktop
|
|
'2xl': 10 // large desktop
|
|
};
|
|
|
|
// Reduced columns when preview panel is open (to maintain thumbnail size)
|
|
const reducedColumns = {
|
|
sm: 1, // mobile
|
|
md: 2, // tablet
|
|
lg: 3, // laptop
|
|
xl: 4, // desktop
|
|
'2xl': 6 // large desktop
|
|
};
|
|
|
|
const columns = showPreviewPanel.value ? reducedColumns : baseColumns;
|
|
|
|
// Determine screen size (this is a simplified approach)
|
|
const width = window.innerWidth;
|
|
if (width >= 1536) return columns['2xl'];
|
|
if (width >= 1280) return columns.xl;
|
|
if (width >= 1024) return columns.lg;
|
|
if (width >= 768) return columns.md;
|
|
return columns.sm;
|
|
});
|
|
|
|
// Enhanced search functionality
|
|
const handleSearch = (query) => {
|
|
searchQuery.value = query;
|
|
if (query.length > 2) {
|
|
// Mock search suggestions
|
|
searchSuggestions.value = [
|
|
'Budget Reports',
|
|
'Annual Budget 2024',
|
|
'Budget Analysis'
|
|
].filter(s => s.toLowerCase().includes(query.toLowerCase()));
|
|
showSearchSuggestions.value = true;
|
|
} else {
|
|
showSearchSuggestions.value = false;
|
|
}
|
|
};
|
|
|
|
// Handle search suggestion selection
|
|
const selectSearchSuggestion = (suggestion) => {
|
|
searchQuery.value = suggestion;
|
|
showSearchSuggestions.value = false;
|
|
|
|
// Add to recent searches if not already there
|
|
if (!recentSearches.value.includes(suggestion)) {
|
|
recentSearches.value.unshift(suggestion);
|
|
if (recentSearches.value.length > 5) {
|
|
recentSearches.value.pop();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Quick actions
|
|
const quickActions = [
|
|
{ id: 'view', label: 'View', icon: 'mdi:eye', variant: 'secondary-outline' },
|
|
{ id: 'download', label: 'Download', icon: 'mdi:download', variant: 'primary' },
|
|
{ id: 'share', label: 'Share', icon: 'mdi:share', variant: 'secondary-outline' },
|
|
{ id: 'edit', label: 'Edit', icon: 'mdi:pencil', variant: 'secondary-outline' }
|
|
];
|
|
|
|
// Handle quick action
|
|
const handleQuickAction = (action, document) => {
|
|
console.log(`${action} action on:`, document.name);
|
|
// Implement actual actions here
|
|
};
|
|
|
|
// Document preview functions
|
|
const openDocumentPreview = (document, mode = 'view') => {
|
|
previewDocument.value = document;
|
|
previewMode.value = mode;
|
|
showDocumentPreview.value = true;
|
|
};
|
|
|
|
const closeDocumentPreview = () => {
|
|
showDocumentPreview.value = false;
|
|
previewDocument.value = null;
|
|
previewMode.value = 'view';
|
|
};
|
|
|
|
// Hover thumbnail functions with cursor positioning
|
|
const showDocumentThumbnail = (event, document) => {
|
|
if (document.type !== 'file') return;
|
|
|
|
clearTimeout(thumbnailHoverTimeout.value);
|
|
thumbnailHoverTimeout.value = setTimeout(() => {
|
|
// Get viewport dimensions
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
// Thumbnail dimensions (approximate)
|
|
const thumbnailWidth = 300;
|
|
const thumbnailHeight = 200;
|
|
|
|
// Default offset from cursor
|
|
const offsetX = 15;
|
|
const offsetY = 15;
|
|
|
|
let x = event.clientX + offsetX;
|
|
let y = event.clientY - offsetY;
|
|
|
|
// Adjust horizontal position if it would go off-screen
|
|
if (x + thumbnailWidth > viewportWidth) {
|
|
x = event.clientX - thumbnailWidth - offsetX;
|
|
}
|
|
|
|
// Adjust vertical position if it would go off-screen
|
|
if (y < 0) {
|
|
y = event.clientY + offsetY;
|
|
} else if (y + thumbnailHeight > viewportHeight) {
|
|
y = viewportHeight - thumbnailHeight - 10;
|
|
}
|
|
|
|
hoverThumbnailPosition.value = { x, y };
|
|
hoverThumbnailDocument.value = document;
|
|
showHoverThumbnail.value = true;
|
|
}, 500); // 500ms delay before showing thumbnail
|
|
};
|
|
|
|
const hideDocumentThumbnail = () => {
|
|
clearTimeout(thumbnailHoverTimeout.value);
|
|
showHoverThumbnail.value = false;
|
|
hoverThumbnailDocument.value = null;
|
|
};
|
|
|
|
const handleDocumentPreviewAction = (action, document) => {
|
|
switch (action) {
|
|
case 'view':
|
|
openDocumentPreview(document, 'view');
|
|
break;
|
|
case 'edit':
|
|
openDocumentPreview(document, 'edit');
|
|
break;
|
|
case 'download':
|
|
downloadDocument(document);
|
|
break;
|
|
case 'share':
|
|
shareDocument(document);
|
|
break;
|
|
default:
|
|
console.log('Unhandled preview action:', action);
|
|
}
|
|
};
|
|
|
|
// Document preview event handlers
|
|
const handleDocumentUpdated = (data) => {
|
|
console.log('Document updated:', data);
|
|
// Update the document in the current folder contents
|
|
const index = currentFolderContents.value.findIndex(item => item.id === data.id);
|
|
if (index !== -1) {
|
|
currentFolderContents.value[index] = { ...currentFolderContents.value[index], ...data };
|
|
}
|
|
|
|
// Add to operation history
|
|
addOperation('update', { document: data, path: currentPath.value });
|
|
};
|
|
|
|
const handleAnnotationAdded = (annotation) => {
|
|
console.log('Annotation added:', annotation);
|
|
// Handle annotation added - could sync with server
|
|
};
|
|
|
|
const handleAnnotationUpdated = (annotation) => {
|
|
console.log('Annotation updated:', annotation);
|
|
// Handle annotation updated - could sync with server
|
|
};
|
|
|
|
const handleAnnotationDeleted = (annotationId) => {
|
|
console.log('Annotation deleted:', annotationId);
|
|
// Handle annotation deleted - could sync with server
|
|
};
|
|
|
|
const handleDocumentShared = (shareData) => {
|
|
console.log('Document shared:', shareData);
|
|
// Handle document sharing - could update permissions
|
|
};
|
|
|
|
const handleTagAdded = (tag) => {
|
|
console.log('Tag added:', tag);
|
|
// Handle tag added to document
|
|
if (previewDocument.value) {
|
|
if (!previewDocument.value.tags) {
|
|
previewDocument.value.tags = [];
|
|
}
|
|
previewDocument.value.tags.push(tag);
|
|
}
|
|
};
|
|
|
|
const handleTagRemoved = (tag) => {
|
|
console.log('Tag removed:', tag);
|
|
// Handle tag removed from document
|
|
if (previewDocument.value && previewDocument.value.tags) {
|
|
previewDocument.value.tags = previewDocument.value.tags.filter(t => t !== tag);
|
|
}
|
|
};
|
|
|
|
// Enhanced download and share functions
|
|
const downloadDocument = (document) => {
|
|
// Create download link
|
|
const link = document.createElement('a');
|
|
link.href = `/api/documents/${document.id}/download`;
|
|
link.download = document.name;
|
|
link.click();
|
|
|
|
console.log('Downloading:', document.name);
|
|
|
|
// Add to operation history
|
|
addOperation('download', { document, path: currentPath.value });
|
|
};
|
|
|
|
const shareDocument = (document) => {
|
|
console.log('Sharing:', document.name);
|
|
// Implement share functionality - could open share modal
|
|
// For now, just show a simple alert
|
|
alert(`Share functionality for "${document.name}" would be implemented here.`);
|
|
};
|
|
|
|
// Filtered document structure based on active tab
|
|
const filteredDocumentStructure = computed(() => {
|
|
if (activeTab.value === 'all') {
|
|
return documentStructure.value;
|
|
}
|
|
|
|
return documentStructure.value.filter(cabinet => {
|
|
// Check if cabinet or any of its children match the access level
|
|
const cabinetMatches = cabinet.accessLevel === activeTab.value;
|
|
const hasMatchingChildren = hasAccessLevelInChildren(cabinet, activeTab.value);
|
|
return cabinetMatches || hasMatchingChildren;
|
|
}).map(cabinet => {
|
|
// Filter children recursively
|
|
return {
|
|
...cabinet,
|
|
children: filterChildrenByAccessLevel(cabinet.children, activeTab.value)
|
|
};
|
|
});
|
|
});
|
|
|
|
const hasAccessLevelInChildren = (item, accessLevel) => {
|
|
if (!item.children) return false;
|
|
|
|
return item.children.some(child =>
|
|
child.accessLevel === accessLevel || hasAccessLevelInChildren(child, accessLevel)
|
|
);
|
|
};
|
|
|
|
const filterChildrenByAccessLevel = (children, accessLevel) => {
|
|
if (!children) return [];
|
|
|
|
return children.filter(child => {
|
|
const childMatches = child.accessLevel === accessLevel;
|
|
const hasMatchingChildren = hasAccessLevelInChildren(child, accessLevel);
|
|
return childMatches || hasMatchingChildren;
|
|
}).map(child => ({
|
|
...child,
|
|
children: filterChildrenByAccessLevel(child.children, accessLevel)
|
|
}));
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="dms-explorer h-screen flex flex-col bg-gray-50 dark:bg-gray-900">
|
|
<!-- Windows Explorer-style Title Bar -->
|
|
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-3 py-1.5">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-2">
|
|
<Icon name="mdi:folder-multiple" class="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Document Management System</h1>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<button
|
|
@click="showPreviewPanel = !showPreviewPanel"
|
|
:class="[
|
|
'px-2 py-1 text-sm rounded-md transition-colors',
|
|
showPreviewPanel
|
|
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
]"
|
|
title="Toggle preview panel"
|
|
>
|
|
<Icon name="mdi:dock-right" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Toolbar -->
|
|
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-3 py-2">
|
|
<div class="flex items-center space-x-3">
|
|
<!-- Navigation Buttons -->
|
|
<div class="flex items-center space-x-0.5">
|
|
<button
|
|
@click="goBack"
|
|
:disabled="!canGoBack"
|
|
class="p-1.5 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
|
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
title="Back (Alt+←)"
|
|
>
|
|
<Icon name="mdi:arrow-left" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
@click="goForward"
|
|
:disabled="!canGoForward"
|
|
class="p-1.5 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
|
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
title="Forward (Alt+→)"
|
|
>
|
|
<Icon name="mdi:arrow-right" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
@click="goUp"
|
|
:disabled="currentPath === '/'"
|
|
class="p-1.5 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
|
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
title="Up (Alt+↑)"
|
|
>
|
|
<Icon name="mdi:arrow-up" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Breadcrumb Navigation -->
|
|
<div class="flex-1 bg-gray-50 dark:bg-gray-700 rounded-md px-2 py-1.5 border border-gray-200 dark:border-gray-600">
|
|
<nav class="flex items-center space-x-1 text-sm">
|
|
<button
|
|
v-for="(crumb, index) in breadcrumbPath"
|
|
:key="index"
|
|
@click="navigateToPath(crumb.path)"
|
|
:class="[
|
|
'px-1.5 py-0.5 rounded-md transition-colors flex items-center space-x-1',
|
|
index === breadcrumbPath.length - 1
|
|
? 'text-gray-900 dark:text-gray-100 font-medium bg-white dark:bg-gray-600'
|
|
: 'text-blue-600 dark:text-blue-400 hover:bg-white dark:hover:bg-gray-600'
|
|
]"
|
|
>
|
|
<Icon
|
|
:name="crumb.type === 'root' ? 'mdi:home' :
|
|
crumb.type === 'cabinet' ? 'mdi:archive' :
|
|
crumb.type === 'drawer' ? 'mdi:folder-multiple' :
|
|
'mdi:folder'"
|
|
class="w-3 h-3"
|
|
/>
|
|
<span>{{ crumb.name }}</span>
|
|
</button>
|
|
<Icon
|
|
v-if="index < breadcrumbPath.length - 1"
|
|
name="mdi:chevron-right"
|
|
class="w-3 h-3 text-gray-400"
|
|
/>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Search Box -->
|
|
<div class="relative w-72">
|
|
<div class="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
|
|
<Icon name="mdi:magnify" class="h-4 w-4 text-gray-400" />
|
|
</div>
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="Search documents..."
|
|
class="block w-full pl-9 pr-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md
|
|
bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm
|
|
focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Undo/Redo Buttons -->
|
|
<div class="flex items-center space-x-0.5">
|
|
<button
|
|
@click="undo"
|
|
:disabled="!canUndo"
|
|
class="p-1.5 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
|
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
title="Undo (Ctrl+Z)"
|
|
>
|
|
<Icon name="mdi:undo" class="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
@click="redo"
|
|
:disabled="!canRedo"
|
|
class="p-1.5 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
|
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
title="Redo (Ctrl+Y)"
|
|
>
|
|
<Icon name="mdi:redo" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content Area -->
|
|
<div class="flex flex-1 min-h-0">
|
|
<!-- Left Sidebar - Folder Tree -->
|
|
<div class="w-72 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
|
|
<!-- Access Filter Tabs -->
|
|
<div class="border-b border-gray-200 dark:border-gray-700 p-2">
|
|
<h3 class="text-xs font-semibold text-gray-900 dark:text-gray-100 mb-1.5 uppercase tracking-wider">
|
|
Access Filters
|
|
</h3>
|
|
<div class="flex flex-wrap gap-1">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
@click="changeTab(tab.id)"
|
|
:class="[
|
|
'flex items-center px-1.5 py-1 text-xs font-medium rounded-md transition-all duration-200',
|
|
getTabClasses(tab)
|
|
]"
|
|
:title="tab.description"
|
|
>
|
|
<Icon :name="tab.icon" :class="['w-3 h-3 mr-1', getTabIconClasses(tab)]" />
|
|
<span>{{ tab.label }}</span>
|
|
<span class="ml-1 text-xs opacity-75">
|
|
({{ typeof tab.count === 'function' ? tab.count() : tab.count }})
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Tree Structure -->
|
|
<div class="flex-1 overflow-auto p-2">
|
|
<h3 class="text-xs font-semibold text-gray-900 dark:text-gray-100 mb-1.5 uppercase tracking-wider">
|
|
Document Structure
|
|
</h3>
|
|
<div class="space-y-0.5">
|
|
<div
|
|
v-for="item in filteredDocumentStructure"
|
|
:key="item.id"
|
|
class="relative"
|
|
>
|
|
<div
|
|
data-context-item
|
|
@click="navigateToPath(item.path)"
|
|
@contextmenu="handleTreeContextMenu({ event: $event, item })"
|
|
:class="[
|
|
'flex items-center p-1 text-sm rounded-md cursor-pointer transition-all duration-200 group',
|
|
'hover:bg-gray-100 dark:hover:bg-gray-700',
|
|
currentPath === item.path
|
|
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 font-medium'
|
|
: 'text-gray-700 dark:text-gray-300'
|
|
]"
|
|
>
|
|
<!-- Expand/Collapse Button -->
|
|
<button
|
|
v-if="item.children && item.children.length > 0"
|
|
@click="toggleItemExpanded(item.id, $event)"
|
|
class="flex-shrink-0 w-4 h-4 mr-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center"
|
|
>
|
|
<Icon
|
|
:name="isItemExpanded(item.id) ? 'mdi:chevron-down' : 'mdi:chevron-right'"
|
|
class="w-3 h-3"
|
|
/>
|
|
</button>
|
|
<div v-else class="w-5"></div>
|
|
|
|
<!-- Folder Icon -->
|
|
<Icon
|
|
name="mdi:folder"
|
|
class="w-4 h-4 mr-1.5 flex-shrink-0 text-blue-600 dark:text-blue-400"
|
|
/>
|
|
|
|
<!-- Item Name -->
|
|
<span class="flex-1 truncate text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ item.name }}
|
|
</span>
|
|
|
|
<!-- Item Count -->
|
|
<span
|
|
v-if="item.itemCount"
|
|
class="text-xs text-gray-500 dark:text-gray-400 ml-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
{{ item.itemCount }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Nested children with expand/collapse functionality -->
|
|
<div v-if="item.children && isItemExpanded(item.id)" class="ml-3 mt-0.5 space-y-0.5">
|
|
<div
|
|
v-for="child in item.children"
|
|
:key="child.id"
|
|
class="relative"
|
|
>
|
|
<div
|
|
data-context-item
|
|
@click="navigateToPath(child.path)"
|
|
@contextmenu="handleTreeContextMenu({ event: $event, item: child })"
|
|
:class="[
|
|
'flex items-center p-1 text-sm rounded-md cursor-pointer transition-all duration-200 group',
|
|
'hover:bg-gray-100 dark:hover:bg-gray-700',
|
|
currentPath === child.path
|
|
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 font-medium'
|
|
: 'text-gray-700 dark:text-gray-300'
|
|
]"
|
|
>
|
|
<!-- Expand/Collapse Button for child -->
|
|
<button
|
|
v-if="child.children && child.children.length > 0"
|
|
@click="toggleItemExpanded(child.id, $event)"
|
|
class="flex-shrink-0 w-4 h-4 mr-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center"
|
|
>
|
|
<Icon
|
|
:name="isItemExpanded(child.id) ? 'mdi:chevron-down' : 'mdi:chevron-right'"
|
|
class="w-3 h-3"
|
|
/>
|
|
</button>
|
|
<div v-else class="w-5"></div>
|
|
|
|
<Icon
|
|
name="mdi:folder"
|
|
class="w-4 h-4 mr-1.5 flex-shrink-0 text-blue-600 dark:text-blue-400"
|
|
/>
|
|
<span class="flex-1 truncate text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ child.name }}
|
|
</span>
|
|
<span
|
|
v-if="child.itemCount"
|
|
class="text-xs text-gray-500 dark:text-gray-400 ml-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
{{ child.itemCount }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Third level children -->
|
|
<div v-if="child.children && isItemExpanded(child.id)" class="ml-3 mt-0.5 space-y-0.5">
|
|
<div
|
|
v-for="grandchild in child.children"
|
|
:key="grandchild.id"
|
|
data-context-item
|
|
@click="navigateToPath(grandchild.path)"
|
|
@contextmenu="handleTreeContextMenu({ event: $event, item: grandchild })"
|
|
:class="[
|
|
'flex items-center p-1 text-sm rounded-md cursor-pointer transition-all duration-200 group',
|
|
'hover:bg-gray-100 dark:hover:bg-gray-700',
|
|
currentPath === grandchild.path
|
|
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 font-medium'
|
|
: 'text-gray-700 dark:text-gray-300'
|
|
]"
|
|
>
|
|
<div class="w-5"></div>
|
|
<Icon
|
|
name="mdi:folder"
|
|
class="w-4 h-4 mr-1.5 flex-shrink-0 text-blue-600 dark:text-blue-400"
|
|
/>
|
|
<span class="flex-1 truncate text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ grandchild.name }}
|
|
</span>
|
|
<span
|
|
v-if="grandchild.itemCount"
|
|
class="text-xs text-gray-500 dark:text-gray-400 ml-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
{{ grandchild.itemCount }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content Panel -->
|
|
<div class="flex-1 flex flex-col min-h-0">
|
|
<!-- Action Toolbar -->
|
|
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-3 py-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-3">
|
|
<h2 class="text-base font-semibold text-gray-800 dark:text-gray-200">
|
|
{{ breadcrumbPath[breadcrumbPath.length - 1]?.name || 'Documents' }}
|
|
</h2>
|
|
|
|
<!-- Sort Options -->
|
|
<select
|
|
v-model="sortBy"
|
|
class="text-sm border border-gray-300 dark:border-gray-600 rounded-md px-2 py-1
|
|
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-medium
|
|
focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="name">Sort by Name</option>
|
|
<option value="date">Sort by Date</option>
|
|
<option value="size">Sort by Size</option>
|
|
<option value="type">Sort by Type</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-1.5">
|
|
<!-- View Mode Toggle -->
|
|
<div class="inline-flex rounded-lg border border-gray-200 dark:border-gray-600 p-0.5 bg-gray-50 dark:bg-gray-800">
|
|
<button
|
|
@click="viewMode = 'list'"
|
|
:class="[
|
|
'px-2 py-1 text-sm font-medium rounded-md transition-all',
|
|
viewMode === 'list'
|
|
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
|
]"
|
|
title="List View"
|
|
>
|
|
<Icon name="mdi:view-list" class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
@click="viewMode = 'grid'"
|
|
:class="[
|
|
'px-2 py-1 text-sm font-medium rounded-md transition-all',
|
|
viewMode === 'grid'
|
|
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
|
]"
|
|
title="Grid View"
|
|
>
|
|
<Icon name="mdi:view-grid" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<RsButton
|
|
variant="secondary-outline"
|
|
size="sm"
|
|
@click="openUploadModal"
|
|
title="Upload files (Ctrl+U)"
|
|
class="text-sm font-medium px-3 py-1"
|
|
>
|
|
<Icon name="mdi:upload" class="w-4 h-4 mr-1.5" />
|
|
Upload
|
|
</RsButton>
|
|
|
|
<RsButton
|
|
variant="primary"
|
|
size="sm"
|
|
@click="openCreateNewDialog"
|
|
title="Create new item (Ctrl+Shift+N)"
|
|
class="text-sm font-medium px-3 py-1"
|
|
>
|
|
<Icon name="mdi:plus" class="w-4 h-4 mr-1.5" />
|
|
Create New
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Area with Preview Panel -->
|
|
<div class="flex flex-1 min-h-0">
|
|
<!-- File/Folder Content Area -->
|
|
<div :class="['flex-1 overflow-auto', showPreviewPanel ? 'border-r border-gray-200 dark:border-gray-700' : '']">
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="flex items-center justify-center h-64">
|
|
<div class="text-center">
|
|
<Icon name="mdi:loading" class="w-8 h-8 animate-spin text-gray-400 mx-auto mb-2" />
|
|
<p class="text-gray-500 dark:text-gray-400">Loading...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else-if="currentFolderContents.length === 0" class="flex items-center justify-center h-64">
|
|
<div class="text-center">
|
|
<Icon name="mdi:folder-open" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">This folder is empty</h3>
|
|
<p class="text-gray-500 dark:text-gray-400">Drop files here or use the upload button to add content</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Grid/List -->
|
|
<div v-else class="p-2">
|
|
<!-- List View -->
|
|
<div v-if="viewMode === 'list'" class="space-y-0.5">
|
|
<div
|
|
v-for="item in currentFolderContents"
|
|
:key="item.id"
|
|
data-context-item
|
|
@click="selectItem(item)"
|
|
@dblclick="handleItemDoubleClick(item)"
|
|
@contextmenu="handleDocumentContextMenu($event, item)"
|
|
@dragstart="handleDragStart($event, item)"
|
|
@dragover="handleDragOver($event, item)"
|
|
@dragleave="handleDragLeave"
|
|
@drop="handleDrop($event, item)"
|
|
@mouseenter="showDocumentThumbnail($event, item)"
|
|
@mouseleave="hideDocumentThumbnail"
|
|
draggable="true"
|
|
:class="[
|
|
'flex items-center p-2 rounded-lg border transition-all duration-200 cursor-pointer group relative',
|
|
selectedItem?.id === item.id
|
|
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-700'
|
|
: selectedItems.has(item.id)
|
|
? 'bg-blue-25 dark:bg-blue-900/10 border-blue-200 dark:border-blue-800'
|
|
: 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md',
|
|
dropTarget?.id === item.id ? 'ring-2 ring-blue-500' : ''
|
|
]"
|
|
>
|
|
<!-- Selection Checkbox -->
|
|
<input
|
|
type="checkbox"
|
|
:checked="selectedItems.has(item.id)"
|
|
@change="toggleItemSelection(item)"
|
|
@click.stop
|
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded mr-2"
|
|
/>
|
|
|
|
<!-- Icon -->
|
|
<Icon
|
|
:name="item.type === 'folder' || item.type === 'drawer' || item.type === 'cabinet'
|
|
? 'mdi:folder'
|
|
: getFileTypeIcon(item.name)"
|
|
:class="[
|
|
'w-7 h-7 mr-3 flex-shrink-0',
|
|
item.type === 'folder' || item.type === 'drawer' || item.type === 'cabinet'
|
|
? 'text-blue-600 dark:text-blue-400'
|
|
: getFileTypeColor(item.name)
|
|
]"
|
|
/>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
|
|
{{ item.name }}
|
|
</h3>
|
|
<div class="flex items-center space-x-3 text-xs text-gray-500 dark:text-gray-400">
|
|
<span>{{ formatDate(item.lastModified) }}</span>
|
|
<span v-if="item.size">{{ item.size }}</span>
|
|
<span v-if="item.itemCount">{{ item.itemCount }} items</span>
|
|
</div>
|
|
</div>
|
|
<p v-if="item.description" class="text-xs text-gray-600 dark:text-gray-400 mt-0.5 truncate">
|
|
{{ item.description }}
|
|
</p>
|
|
<!-- Tags -->
|
|
<div v-if="item.tags" class="flex flex-wrap gap-1 mt-1">
|
|
<span
|
|
v-for="tag in item.tags.slice(0, 3)"
|
|
:key="tag"
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium
|
|
bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
|
|
>
|
|
{{ tag }}
|
|
</span>
|
|
<span
|
|
v-if="item.tags.length > 3"
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium
|
|
bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
|
|
>
|
|
+{{ item.tags.length - 3 }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions (visible on hover for files) -->
|
|
<div
|
|
v-if="item.type === 'file'"
|
|
class="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center space-x-0.5 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
|
>
|
|
<button
|
|
@click.stop="openDocumentPreview(item, 'view')"
|
|
class="p-1 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
|
|
title="Preview"
|
|
>
|
|
<Icon name="mdi:eye" class="w-3 h-3" />
|
|
</button>
|
|
<button
|
|
@click.stop="downloadDocument(item)"
|
|
class="p-1 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors"
|
|
title="Download"
|
|
>
|
|
<Icon name="mdi:download" class="w-3 h-3" />
|
|
</button>
|
|
<button
|
|
v-if="canEdit"
|
|
@click.stop="openDocumentPreview(item, 'edit')"
|
|
class="p-1 bg-orange-600 hover:bg-orange-700 text-white rounded-md transition-colors"
|
|
title="Edit"
|
|
>
|
|
<Icon name="mdi:pencil" class="w-3 h-3" />
|
|
</button>
|
|
<button
|
|
@click.stop="shareDocument(item)"
|
|
class="p-1 bg-purple-600 hover:bg-purple-700 text-white rounded-md transition-colors"
|
|
title="Share"
|
|
>
|
|
<Icon name="mdi:share" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grid View -->
|
|
<div
|
|
v-else
|
|
class="grid gap-2"
|
|
:style="{ gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))` }"
|
|
>
|
|
<div
|
|
v-for="item in currentFolderContents"
|
|
:key="item.id"
|
|
data-context-item
|
|
@click="selectItem(item)"
|
|
@dblclick="handleItemDoubleClick(item)"
|
|
@contextmenu="handleDocumentContextMenu($event, item)"
|
|
@dragstart="handleDragStart($event, item)"
|
|
@dragover="handleDragOver($event, item)"
|
|
@dragleave="handleDragLeave"
|
|
@drop="handleDrop($event, item)"
|
|
draggable="true"
|
|
:class="[
|
|
'p-3 rounded-lg border transition-all duration-200 cursor-pointer text-center group relative',
|
|
selectedItem?.id === item.id
|
|
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-700'
|
|
: selectedItems.has(item.id)
|
|
? 'bg-blue-25 dark:bg-blue-900/10 border-blue-200 dark:border-blue-800'
|
|
: 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700',
|
|
dropTarget?.id === item.id ? 'ring-2 ring-blue-500' : ''
|
|
]"
|
|
>
|
|
<!-- Selection Checkbox -->
|
|
<div class="absolute top-1.5 left-1.5">
|
|
<input
|
|
type="checkbox"
|
|
:checked="selectedItems.has(item.id)"
|
|
@change="toggleItemSelection(item)"
|
|
@click.stop
|
|
class="w-3 h-3 text-blue-600 bg-gray-100 border-gray-300 rounded"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Icon -->
|
|
<Icon
|
|
:name="item.type === 'folder' || item.type === 'drawer' || item.type === 'cabinet'
|
|
? 'mdi:folder'
|
|
: getFileTypeIcon(item.name)"
|
|
:class="[
|
|
'w-10 h-10 mx-auto mb-1.5',
|
|
item.type === 'folder' || item.type === 'drawer' || item.type === 'cabinet'
|
|
? 'text-blue-600 dark:text-blue-400'
|
|
: getFileTypeColor(item.name)
|
|
]"
|
|
/>
|
|
|
|
<!-- Name -->
|
|
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
{{ item.name }}
|
|
</h3>
|
|
|
|
<!-- Metadata -->
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
<div>{{ formatDate(item.lastModified) }}</div>
|
|
<div v-if="item.size">{{ item.size }}</div>
|
|
<div v-if="item.itemCount">{{ item.itemCount }} items</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Panel -->
|
|
<div v-if="showPreviewPanel" class="w-80 bg-white dark:bg-gray-800 flex flex-col">
|
|
<div class="border-b border-gray-200 dark:border-gray-700 p-3">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
{{ selectedItem ? 'Preview' : 'Properties' }}
|
|
</h3>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-auto p-3">
|
|
<div v-if="selectedItem">
|
|
<!-- File/Folder Info -->
|
|
<div class="mb-4">
|
|
<div class="flex items-center space-x-2.5 mb-2.5">
|
|
<Icon
|
|
:name="selectedItem.type === 'folder' || selectedItem.type === 'drawer' || selectedItem.type === 'cabinet'
|
|
? 'mdi:folder'
|
|
: getFileTypeIcon(selectedItem.name)"
|
|
:class="[
|
|
'w-7 h-7',
|
|
selectedItem.type === 'folder' || selectedItem.type === 'drawer' || selectedItem.type === 'cabinet'
|
|
? 'text-blue-600 dark:text-blue-400'
|
|
: getFileTypeColor(selectedItem.name)
|
|
]"
|
|
/>
|
|
<div>
|
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
{{ selectedItem.name }}
|
|
</h4>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
{{ selectedItem.type === 'file' ? 'File' : 'Folder' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Properties -->
|
|
<div class="space-y-1.5 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="font-medium text-gray-600 dark:text-gray-400">Size:</span>
|
|
<span class="font-medium text-gray-900 dark:text-gray-100">
|
|
{{ selectedItem.size || (selectedItem.itemCount ? `${selectedItem.itemCount} items` : 'Unknown') }}
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="font-medium text-gray-600 dark:text-gray-400">Modified:</span>
|
|
<span class="font-medium text-gray-900 dark:text-gray-100">
|
|
{{ formatDate(selectedItem.lastModified) }}
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="font-medium text-gray-600 dark:text-gray-400">Owner:</span>
|
|
<span class="font-medium text-gray-900 dark:text-gray-100">
|
|
{{ selectedItem.owner || 'Unknown' }}
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="font-medium text-gray-600 dark:text-gray-400">Access:</span>
|
|
<span class="font-medium text-gray-900 dark:text-gray-100 capitalize">
|
|
{{ selectedItem.accessLevel || 'Unknown' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div v-if="selectedItem.tags" class="mt-3">
|
|
<label class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Tags</label>
|
|
<div class="flex flex-wrap gap-1 mt-1.5">
|
|
<span
|
|
v-for="tag in selectedItem.tags"
|
|
:key="tag"
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium
|
|
bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-400"
|
|
>
|
|
{{ tag }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Content -->
|
|
<div v-if="previewContent" class="mt-4">
|
|
<h5 class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2">
|
|
Preview
|
|
</h5>
|
|
|
|
<!-- Image Preview -->
|
|
<div v-if="previewContent.type === 'image'" class="text-center">
|
|
<img
|
|
:src="previewContent.url"
|
|
:alt="selectedItem.name"
|
|
class="max-w-full h-auto rounded-md border border-gray-200 dark:border-gray-600"
|
|
/>
|
|
</div>
|
|
|
|
<!-- PDF Preview -->
|
|
<div v-else-if="previewContent.type === 'pdf'" class="text-center">
|
|
<Icon name="mdi:file-pdf-box" class="w-14 h-14 text-red-600 dark:text-red-400 mx-auto mb-1.5" />
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
PDF Document
|
|
</p>
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-500">
|
|
{{ previewContent.metadata?.pages || 'Unknown' }} pages
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Text Preview -->
|
|
<div v-else-if="previewContent.type === 'text'" class="text-sm">
|
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-md p-2.5 border border-gray-200 dark:border-gray-600">
|
|
<pre class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{{ previewContent.content.substring(0, 200) }}...</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata Preview -->
|
|
<div v-else class="text-sm space-y-1.5">
|
|
<div v-for="(value, key) in previewContent.metadata" :key="key" class="flex justify-between">
|
|
<span class="font-medium text-gray-600 dark:text-gray-400 capitalize">{{ key }}:</span>
|
|
<span class="font-medium text-gray-900 dark:text-gray-100">{{ value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No Selection State -->
|
|
<div v-else class="text-center py-6">
|
|
<Icon name="mdi:information-outline" class="w-10 h-10 text-gray-300 mx-auto mb-1.5" />
|
|
<p class="text-gray-500 dark:text-gray-400 text-sm font-medium">
|
|
Select a file or folder to view its properties
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enhanced Dialogs -->
|
|
<UploadWithMetadataModal
|
|
v-model:visible="showUploadModal"
|
|
:target-folder="{
|
|
id: 'current',
|
|
name: breadcrumbPath[breadcrumbPath.length - 1]?.name || 'Root',
|
|
path: currentPath
|
|
}"
|
|
:folder-structure="documentStructure"
|
|
@upload-complete="handleUploadComplete"
|
|
@cancel="showUploadModal = false"
|
|
/>
|
|
|
|
<CreateNewDialog
|
|
v-model:visible="showCreateNewDialog"
|
|
:initial-path="currentPath"
|
|
@create-complete="handleCreateComplete"
|
|
@cancel="showCreateNewDialog = false"
|
|
/>
|
|
|
|
<ContextMenu
|
|
v-model:visible="showContextMenu"
|
|
:x="contextMenuPosition.x"
|
|
:y="contextMenuPosition.y"
|
|
:target="contextMenuTarget"
|
|
@close="showContextMenu = false"
|
|
@item-click="handleContextMenuAction"
|
|
/>
|
|
|
|
<!-- Document Preview Dialog -->
|
|
<DocumentPreviewDialog
|
|
v-model:visible="showDocumentPreview"
|
|
:document="previewDocument"
|
|
:initial-mode="previewMode"
|
|
@close="closeDocumentPreview"
|
|
@document-updated="handleDocumentUpdated"
|
|
@annotation-added="handleAnnotationAdded"
|
|
@annotation-updated="handleAnnotationUpdated"
|
|
@annotation-deleted="handleAnnotationDeleted"
|
|
@document-shared="handleDocumentShared"
|
|
@tag-added="handleTagAdded"
|
|
@tag-removed="handleTagRemoved"
|
|
/>
|
|
|
|
<!-- Hover Thumbnail -->
|
|
<div
|
|
v-if="showHoverThumbnail && hoverThumbnailDocument"
|
|
class="fixed z-40 pointer-events-none"
|
|
:style="{
|
|
left: hoverThumbnailPosition.x + 'px',
|
|
top: hoverThumbnailPosition.y + 'px'
|
|
}"
|
|
>
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-600 p-3 max-w-xs">
|
|
<!-- Thumbnail Preview -->
|
|
<div class="flex items-center space-x-3 mb-2">
|
|
<Icon
|
|
:name="getFileTypeIcon(hoverThumbnailDocument.name)"
|
|
:class="['w-8 h-8', getFileTypeColor(hoverThumbnailDocument.name)]"
|
|
/>
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
{{ hoverThumbnailDocument.name }}
|
|
</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ hoverThumbnailDocument.size }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Type Specific Preview -->
|
|
<div class="bg-gray-100 dark:bg-gray-700 rounded-md p-2 mb-2">
|
|
<!-- PDF Preview -->
|
|
<div v-if="hoverThumbnailDocument.name?.toLowerCase().endsWith('.pdf')" class="text-center">
|
|
<Icon name="mdi:file-pdf-box" class="w-12 h-12 text-red-500 mx-auto mb-1" />
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">PDF Document</p>
|
|
</div>
|
|
|
|
<!-- Word Document Preview -->
|
|
<div v-else-if="hoverThumbnailDocument.name?.toLowerCase().match(/\.(doc|docx)$/)" class="text-center">
|
|
<Icon name="mdi:file-word-box" class="w-12 h-12 text-blue-500 mx-auto mb-1" />
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Word Document</p>
|
|
</div>
|
|
|
|
<!-- Excel Preview -->
|
|
<div v-else-if="hoverThumbnailDocument.name?.toLowerCase().match(/\.(xls|xlsx)$/)" class="text-center">
|
|
<Icon name="mdi:file-excel-box" class="w-12 h-12 text-green-500 mx-auto mb-1" />
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Excel Spreadsheet</p>
|
|
</div>
|
|
|
|
<!-- Image Preview -->
|
|
<div v-else-if="hoverThumbnailDocument.name?.toLowerCase().match(/\.(jpg|jpeg|png|gif)$/)" class="text-center">
|
|
<Icon name="mdi:file-image" class="w-12 h-12 text-purple-500 mx-auto mb-1" />
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Image File</p>
|
|
</div>
|
|
|
|
<!-- Generic File Preview -->
|
|
<div v-else class="text-center">
|
|
<Icon name="mdi:file-document" class="w-12 h-12 text-gray-500 mx-auto mb-1" />
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Document</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Metadata -->
|
|
<div class="space-y-1 text-xs">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Modified:</span>
|
|
<span class="text-gray-900 dark:text-gray-100">{{ formatDate(hoverThumbnailDocument.lastModified) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Owner:</span>
|
|
<span class="text-gray-900 dark:text-gray-100">{{ hoverThumbnailDocument.owner || 'Unknown' }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Access:</span>
|
|
<span class="text-gray-900 dark:text-gray-100 capitalize">{{ hoverThumbnailDocument.accessLevel }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Action Hint -->
|
|
<div class="mt-2 pt-2 border-t border-gray-200 dark:border-gray-600">
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 text-center">
|
|
Double-click to preview • Right-click for options
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Windows Explorer Styling with standardized typography */
|
|
.dms-explorer {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
background: #f8fafc;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
line-height: 1.5;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dark .dms-explorer {
|
|
background: #0f172a;
|
|
}
|
|
|
|
/* Typography standardization */
|
|
.dms-explorer h1,
|
|
.dms-explorer h2,
|
|
.dms-explorer h3,
|
|
.dms-explorer h4,
|
|
.dms-explorer h5,
|
|
.dms-explorer h6 {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.dms-explorer p,
|
|
.dms-explorer span,
|
|
.dms-explorer div {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
}
|
|
|
|
.dms-explorer button {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dms-explorer input,
|
|
.dms-explorer select,
|
|
.dms-explorer textarea {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Grid-based layout */
|
|
.dms-explorer * {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Enhanced hover effects */
|
|
.group:hover .group-hover\:opacity-100 {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* File type specific styling */
|
|
.file-icon {
|
|
transition: transform 0.2s ease-in-out;
|
|
}
|
|
|
|
.file-icon:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* Breadcrumb styling */
|
|
.breadcrumb-segment {
|
|
position: relative;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
|
|
.breadcrumb-segment:hover {
|
|
background-color: rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
/* Selection styling */
|
|
.item-selected {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
/* Drag and drop styling */
|
|
.drag-over {
|
|
background: rgba(59, 130, 246, 0.05);
|
|
border: 2px dashed #3b82f6;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.dragging {
|
|
opacity: 0.6;
|
|
transform: rotate(5deg);
|
|
}
|
|
|
|
/* List view styling */
|
|
.list-item {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
border-radius: 6px;
|
|
transition: all 0.15s ease-in-out;
|
|
}
|
|
|
|
.list-item:hover {
|
|
background-color: rgba(0, 0, 0, 0.03);
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.dark .list-item:hover {
|
|
background-color: rgba(255, 255, 255, 0.03);
|
|
}
|
|
|
|
/* Grid view styling */
|
|
.grid-item {
|
|
aspect-ratio: 1;
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
transition: all 0.2s ease-in-out;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.grid-item:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.dark .grid-item:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
/* Toolbar styling */
|
|
.toolbar {
|
|
background: linear-gradient(to bottom, #ffffff, #f8fafc);
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.dark .toolbar {
|
|
background: linear-gradient(to bottom, #1e293b, #0f172a);
|
|
border-bottom: 1px solid #334155;
|
|
}
|
|
|
|
/* Navigation buttons */
|
|
.nav-button {
|
|
position: relative;
|
|
padding: 8px;
|
|
border-radius: 6px;
|
|
transition: all 0.15s ease-in-out;
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.nav-button:hover:not(:disabled) {
|
|
background: rgba(0, 0, 0, 0.05);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.nav-button:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.dark .nav-button:hover:not(:disabled) {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
/* Search box styling */
|
|
.search-input {
|
|
background: #ffffff;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
padding: 8px 12px 8px 40px;
|
|
transition: all 0.2s ease-in-out;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.search-input:focus {
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
outline: none;
|
|
}
|
|
|
|
.dark .search-input {
|
|
background: #374151;
|
|
border-color: #4b5563;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.dark .search-input:focus {
|
|
border-color: #60a5fa;
|
|
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
|
}
|
|
|
|
/* Sidebar styling */
|
|
.sidebar {
|
|
background: #ffffff;
|
|
border-right: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.dark .sidebar {
|
|
background: #1f2937;
|
|
border-right: 1px solid #374151;
|
|
}
|
|
|
|
/* Tree view styling */
|
|
.tree-item {
|
|
padding: 6px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease-in-out;
|
|
}
|
|
|
|
.tree-item:hover {
|
|
background: rgba(59, 130, 246, 0.05);
|
|
}
|
|
|
|
.tree-item.active {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
color: #1e40af;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dark .tree-item.active {
|
|
color: #93c5fd;
|
|
}
|
|
|
|
/* Preview panel styling */
|
|
.preview-panel {
|
|
background: #ffffff;
|
|
border-left: 1px solid #e5e7eb;
|
|
min-height: 0;
|
|
}
|
|
|
|
.dark .preview-panel {
|
|
background: #1f2937;
|
|
border-left: 1px solid #374151;
|
|
}
|
|
|
|
/* Button styling */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding: 8px 16px;
|
|
border: 1px solid transparent;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
line-height: 1.5;
|
|
text-decoration: none;
|
|
transition: all 0.15s ease-in-out;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #3b82f6;
|
|
color: #ffffff;
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: #2563eb;
|
|
border-color: #2563eb;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #ffffff;
|
|
color: #374151;
|
|
border-color: #d1d5db;
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: #f9fafb;
|
|
border-color: #9ca3af;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.dark .btn-secondary {
|
|
background: #374151;
|
|
color: #f9fafb;
|
|
border-color: #4b5563;
|
|
}
|
|
|
|
.dark .btn-secondary:hover:not(:disabled) {
|
|
background: #4b5563;
|
|
border-color: #6b7280;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
|
|
.dark .custom-scrollbar::-webkit-scrollbar-track {
|
|
background: #334155;
|
|
}
|
|
|
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: #475569;
|
|
}
|
|
|
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: #64748b;
|
|
}
|
|
|
|
/* Animation classes */
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideInFromLeft {
|
|
from { transform: translateX(-100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideInFromRight {
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
|
|
@keyframes scaleIn {
|
|
from { transform: scale(0.9); opacity: 0; }
|
|
to { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
.animate-fade-in {
|
|
animation: fadeIn 0.3s ease-out;
|
|
}
|
|
|
|
.animate-slide-in-left {
|
|
animation: slideInFromLeft 0.3s ease-out;
|
|
}
|
|
|
|
.animate-slide-in-right {
|
|
animation: slideInFromRight 0.3s ease-out;
|
|
}
|
|
|
|
.animate-scale-in {
|
|
animation: scaleIn 0.2s ease-out;
|
|
}
|
|
|
|
/* Context menu styling */
|
|
.context-menu {
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
padding: 4px;
|
|
min-width: 200px;
|
|
z-index: 9999;
|
|
position: fixed;
|
|
}
|
|
|
|
.dark .context-menu {
|
|
background: #374151;
|
|
border-color: #4b5563;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.context-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease-in-out;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.context-menu-item:hover {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
color: #1e40af;
|
|
}
|
|
|
|
.dark .context-menu-item:hover {
|
|
background: rgba(96, 165, 250, 0.1);
|
|
color: #93c5fd;
|
|
}
|
|
|
|
.context-menu-separator {
|
|
height: 1px;
|
|
background: #e5e7eb;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.dark .context-menu-separator {
|
|
background: #4b5563;
|
|
}
|
|
|
|
/* Loading and empty states */
|
|
.loading-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 64px 32px;
|
|
text-align: center;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.dark .loading-state,
|
|
.dark .empty-state {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 768px) {
|
|
.dms-explorer {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.grid-cols-6 {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
|
|
.w-80 {
|
|
width: 100%;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100%;
|
|
z-index: 50;
|
|
transform: translateX(-100%);
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
|
|
.w-80.open {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
/* Accessibility */
|
|
.focus\:ring-2:focus {
|
|
outline: 2px solid transparent;
|
|
outline-offset: 2px;
|
|
box-shadow: 0 0 0 2px #3b82f6;
|
|
}
|
|
|
|
/* High contrast mode support */
|
|
@media (prefers-contrast: high) {
|
|
.border-gray-200 {
|
|
border-color: #000000;
|
|
}
|
|
|
|
.text-gray-600 {
|
|
color: #000000;
|
|
}
|
|
|
|
.bg-gray-50 {
|
|
background-color: #ffffff;
|
|
}
|
|
}
|
|
|
|
/* Print styles */
|
|
@media print {
|
|
.dms-explorer {
|
|
background: white;
|
|
color: black;
|
|
}
|
|
|
|
.sidebar,
|
|
.preview-panel {
|
|
display: none;
|
|
}
|
|
|
|
.main-content {
|
|
width: 100%;
|
|
}
|
|
}
|
|
</style> |