generated from corrad-software/corrad-af-2024

The current API endpoint data doesn't match the document structure in the frontend logic. So I introduced a mapping function but it has not been called into the code yet as this needs a major refactoring to do.
2727 lines
87 KiB
Vue
2727 lines
87 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;
|
|
};
|
|
|
|
const mapAPIToDocumentStructure = async () => {
|
|
const response = await fetch('/api/dms/folder');
|
|
const APIData = await response.json();
|
|
|
|
// const documentStructure.value = APIData.map(obj => {
|
|
// id: obj.cb_id,
|
|
// name: obj.cb_name,
|
|
// type: obj.cb_type || "folder",
|
|
// parent_id: obj.cb_parent_id,
|
|
// access_level: obj.cb_access_level || "public",
|
|
// itemCount: null,
|
|
// children: obj.children_count || null
|
|
// })
|
|
|
|
return documentStructure.value;
|
|
}
|
|
|
|
// Navigation functions
|
|
const buildBreadcrumbs = (path) => {
|
|
if (path === '/') {
|
|
return [{ name: 'Root', path: '/', type: 'root' }];
|
|
}
|
|
|
|
const crumbs = [{ name: 'Root', path: '/', type: 'root' }];
|
|
const segments = path.split('/').filter(Boolean);
|
|
|
|
// const documentStructure.value = await mapAPIToDocumentStructure();
|
|
let itemsToSearch = documentStructure.value;
|
|
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);
|
|
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(() => {
|
|
checkMobileView();
|
|
|
|
// Add event listeners
|
|
window.addEventListener('resize', updateGridLayout);
|
|
window.addEventListener('keydown', handleKeyboardShortcuts);
|
|
document.addEventListener('click', handleGlobalClick);
|
|
document.addEventListener('contextmenu', handleGlobalContextMenu);
|
|
|
|
// 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> |