Files
lucia-frontend/src/views/Files/index.vue
2024-02-21 18:29:52 +08:00

591 lines
26 KiB
Vue

<template>
<div class="container pt-4 2xl:max-w-none">
<!-- Recently Used & Performance Comparison -->
<div>
<!-- Performance Comparison -->
<section v-if="filesTag === 'COMPARE'">
<h2 class="h-12 font-bold py-4 mb-4 border-b border-neutral-500">Performance Comparison</h2>
<div class="flex justify-start items-center gap-4 w-full h-[184px] scrollbar pb-4">
<!-- primaryDrag -->
<div class="relative w-[216px] min-w-[216px] h-full">
<div v-if="primaryDragData.length === 0" class="w-full h-full p-4 border rounded border-neutral-300 duration-300 text-neutral-500 absolute">
<div class="h-full flex flex-col justify-center items-center gap-4">
<p class="text-4xl font-black">1</p>
<p class="text-sm font-medium">Drag and drop a file here</p>
</div>
</div>
<draggable
v-model="primaryDragData" :group="{name: 'files', pull: false, put: primaryDragData.length < 1 }" item-key="id" class="w-full h-full">
<template #item="{element}">
<div class="w-full h-full p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300 flex flex-col justify-between cursor-pointer" :title="element.name" >
<div class="h-full">
<span class="material-symbols-outlined mb-2 text-[32px] block">
{{element.icon}}
</span>
<h3 class="text-sm font-medium mb-2 whitespace-nowrap break-keep overflow-hidden text-ellipsis">
{{element.name}}
</h3>
<p class="text-sm text-neutral-500 whitespace-nowrap break-keep text-ellipsis overflow-hidden">
{{element.parentLog}}
</p>
</div>
<p class="text-sm text-neutral-500">
{{element.updated_at}}
</p>
</div>
</template>
</draggable>
<!-- delete icon -->
<span v-show="primaryDragData.length > 0" class="material-symbols-outlined material-fill text-neutral-500 bg-neutral-10 block rounded-full absolute -top-[5%] -right-[5%] z-50 cursor-pointer hover:text-danger" @click="primaryDragDelete">do_not_disturb_on</span>
</div>
<!-- secondaryData -->
<div class="relative w-[216px] min-w-[216px] h-full">
<div v-show="secondaryDragData.length === 0" class="w-full h-full p-4 border rounded border-neutral-300 duration-300 text-neutral-500 absolute">
<div class="h-full flex flex-col justify-center items-center gap-4">
<p class="text-4xl font-black">2</p>
<p class="text-sm font-medium">Drag and drop a file here</p>
</div>
</div>
<draggable
v-model="secondaryDragData" :group="{name: 'files', pull: false, put: secondaryDragData.length < 1 }" item-key="id" class="w-full h-full">
<template #item="{element}">
<div class="w-full h-full p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300 flex flex-col justify-between cursor-pointer" :title="element.name" >
<div>
<span class="material-symbols-outlined mb-2 text-[32px] block">
{{element.icon}}
</span>
<h3 class="text-sm font-medium mb-2 whitespace-nowrap break-keep overflow-hidden text-ellipsis">
{{element.name}}
</h3>
<p class="text-sm text-neutral-500 whitespace-nowrap break-keep text-ellipsis overflow-hidden">
{{element.parentLog}}
</p>
</div>
<p class="text-sm text-neutral-500">
{{element.updated_at}}
</p>
</div>
</template>
</draggable>
<!-- delete icon -->
<span v-show="secondaryDragData.length > 0" class="material-symbols-outlined material-fill bg-neutral-10 text-neutral-500 block rounded-full absolute -top-[5%] -right-[5%] cursor-pointer hover:text-danger" @click="secondaryDragDelete
">do_not_disturb_on</span>
</div>
<button class="btn btn-sm" :class="this.isCompareDisabledButton ? 'btn-disable' : 'btn-c-primary'" :disabled="isCompareDisabledButton" @click="compareSubmit">Compare</button>
</div>
</section>
<!-- Recently Used -->
<section v-else>
<h2 class="h-12 font-bold py-4 mb-4 border-b border-neutral-500">Recently Used</h2>
<!-- card group 最多六個-->
<ul class="flex justify-start items-center gap-4 overflow-x-auto w-full h-[184px] scrollbar pb-4">
<!-- card item v-for -->
<li class="w-[216px] min-w-[216px] h-full p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300 flex flex-col justify-between cursor-pointer" v-for="(file, index) in recentlyUsedFiles.slice(0, 6)" :key="file.id" @dblclick="enterDiscover(file)" :title="file.name" @contextmenu="onRightClick($event, file)" >
<div>
<span class="material-symbols-outlined mb-2 text-[32px] block">
{{ file.icon }}
</span>
<h3 class="text-sm font-medium mb-2 whitespace-nowrap break-keep overflow-hidden text-ellipsis">
{{ file.name }}
</h3>
<p class="text-sm text-neutral-500 whitespace-nowrap break-keep text-ellipsis overflow-hidden">
{{ file.parentLog }}
</p>
</div>
<p class="text-sm text-neutral-500">
{{ file.accessed_at }}
</p>
</li>
</ul>
</section>
</div>
<!-- All Files -->
<div>
<!-- Compare -->
<section v-if="filesTag === 'COMPARE'">
<!-- All Files header -->
<div class="h-12 mb-4 border-b flex justify-between items-center border-neutral-500">
<h2 class="font-bold">All Files</h2>
<ul class="flex items-center gap-x-4">
<li>
<Dropdown v-model="gridSort" :options="columnType" optionLabel="name" placeholder="Grid Sort" class="w-full !border-neutral-500" inputClass="!text-sm" @change="getGridSortData($event)"></Dropdown>
</li>
<li>
<IconGrid class="hover:bg-neutral-50 duration-300"></IconGrid>
</li>
</ul>
</div>
<!-- All Files type of grid -->
<draggable tag="ul" :list="compareData" :group="{ name: 'files' }" itemKey="name" class="flex justify-start items-start gap-4 flex-wrap overflow-y-scroll overflow-x-hidden max-h-[calc(100vh_-_440px)] scrollbar">
<template #item="{ element }">
<li class="w-[216px] h-[168px] p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300 flex flex-col justify-between cursor-pointer" :title="element.name" >
<div>
<span class="material-symbols-outlined mb-2 text-[32px] block">
{{ element.icon }}
</span>
<h3 class="text-sm font-medium mb-2 whitespace-nowrap break-keep overflow-hidden text-ellipsis">
{{ element.name }}
</h3>
<p class="text-sm text-neutral-500 whitespace-nowrap break-keep text-ellipsis overflow-hidden">
{{ element.parentLog }}
</p>
</div>
<p class="text-sm text-neutral-500">
{{ element.updated_at }}
</p>
</li>
</template>
</draggable>
</section>
<!-- All & Discover -->
<section v-else>
<!-- All Files header -->
<div class="h-12 mb-4 border-b flex justify-between items-center border-neutral-500">
<h2 class="font-bold">All Files</h2>
<ul class="flex items-center gap-x-4">
<li v-show="isActive !== null" class="animate-fadein">
<ul class="flex justify-center items-center gap-x-4 px-4 py-1 rounded-full bg-neutral-200 text-neutral-700">
<li><span class="material-symbols-outlined align-bottom cursor-pointer hover:text-primary" @click="rename">edit_square</span></li>
<li><span class="material-symbols-outlined align-bottom cursor-pointer hover:text-primary" @click="download">download</span></li>
<li><span class="material-symbols-outlined align-bottom cursor-pointer hover:text-primary" @click="deleteFile">delete</span></li>
</ul>
</li>
<li class="cursor-pointer" @click="switchListOrGrid = false">
<IconList class="hover:fill-primary hover:bg-neutral-50 duration-300"></IconList>
</li>
<li class="cursor-pointer" @click="switchListOrGrid = true">
<IconGrid class="hover:fill-primary hover:bg-neutral-50 duration-300"></IconGrid>
</li>
</ul>
</div>
<!-- All Files type of List -->
<div class="overflow-y-scroll overflow-x-hidden scrollbar max-h-[calc(100vh_-_440px)]" v-if="!switchListOrGrid">
<DataTable :value="allFiles" dataKey="id" tableClass="w-full text-sm cursor-pointer relative table-fixed" :rowClass="setRowClass" breakpoint="0" @row-dblclick="enterDiscover($event.data)" contextmenu v-model:contextMenuSelection="selectedTableFile" @row-contextmenu="onRightClickTable">
<Column field="name" header="Name" bodyClass="font-medium fileName" sortable></Column>
<Column field="parentLog" header="Dependency" bodyClass="text-neutral-500" sortable></Column>
<Column field="fileType" header="File Type" bodyClass="text-neutral-500 fileType" sortable></Column>
<Column field="owner.name" header="Owner" bodyClass="text-neutral-500" sortable></Column>
<Column field="updated_at" header="Last Update" bodyClass="text-neutral-500" sortable></Column>
<Column bodyClass="text-neutral-500">
<template #body="slotProps">
<ul class="opacity-0 group-hover:opacity-100 flex justify-end items-center gap-x-2 text-neutral-700">
<li><span class="material-symbols-outlined align-bottom cursor-pointer hover:text-primary" @click="rename(slotProps.data.type, slotProps.data.id, 'list-hover')">edit_square</span></li>
<li><span class="material-symbols-outlined align-bottom cursor-pointer hover:text-primary" @click="download(slotProps.data.type, slotProps.data.id, 'list-hover', slotProps.data.name)">download</span></li>
<li><span class="material-symbols-outlined align-bottom cursor-pointer hover:text-primary" @click="deleteFile(slotProps.data.type, slotProps.data.id, slotProps.data.name, 'list-hover')">delete</span></li>
</ul>
</template>
</Column>
</DataTable>
</div>
<!-- All Files type of grid -->
<ul class="flex justify-start items-start gap-4 flex-wrap overflow-y-scroll overflow-x-hidden max-h-[calc(100vh_-_440px)] scrollbar" v-else>
<li class="w-[216px] h-[168px] p-4 border rounded border-neutral-300 hover:bg-primary/10 hover:border-primary duration-300 flex flex-col justify-between cursor-pointer" v-for="(file, index) in allFiles" :key="file.id" :class="{ 'bg-primary/10 border-primary': isActive === index}" @dblclick="enterDiscover(file)" :title="file.name" @contextmenu="onRightClick($event, file)" @click="onGridCardClick(file, index)" :id="'li' + index">
<div>
<span class="material-symbols-outlined mb-2 text-[32px] block">
{{ file.icon }}
</span>
<h3 class="text-sm font-medium mb-2 whitespace-nowrap break-keep overflow-hidden text-ellipsis">
{{ file.name }}
</h3>
<p class="text-sm text-neutral-500 whitespace-nowrap break-keep text-ellipsis overflow-hidden">
{{ file.parentLog }}
</p>
</div>
<p class="text-sm text-neutral-500">
{{ file.updated_at }}
</p>
</li>
</ul>
</section>
</div>
<!-- ContextMenu -->
<ContextMenu ref="fileRightMenu" :model="items" @hide="selectedFile = null" class="cursor-pointer">
<template #item="{ item }">
<a class="flex align-items-center px-4 py-2 duration-300 hover:bg-primary/20">
<span class="material-symbols-outlined">{{ item.icon }}</span>
<span class="ml-2 text-sm inline-flex items-center">{{ item.label }}</span>
<span v-if="item.shortcut" class="border border-round p-1">{{ item.shortcut }}</span>
</a>
</template>
</ContextMenu>
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoginStore from '@/stores/login.js';
import filesStore from '@/stores/files.js';
import AllMapDataStore from '@/stores/allMapData.js';
import LoadingStore from '@/stores/loading.js';
import IconDataFormat from '@/components/icons/IconDataFormat.vue';
import IconRule from '@/components/icons/IconRule.vue';
import IconsFilter from '@/components/icons/IconsFilter.vue';
import IconFlowChart from '@/components/icons/IconFlowChart.vue';
import IconVector from '@/components/icons/IconVector.vue';
import IconList from '@/components/icons/IconList.vue';
import IconGrid from '@/components/icons/IconGrid.vue';
import { renameModal, deleteFileModal } from '@/module/alertModal.js';
export default {
data() {
return {
isActive: null,
isHover: null,
switchListOrGrid: false,
selectedTableFile: null, // table 右鍵選單 item
selectedFile: null, // 右鍵選單 item
selectedType: null,
selectedId: null,
selecteName: null,
items: [
{
label: 'Rename',
icon: 'edit_square',
command: this.rename,
},
{
label: 'Download',
icon: 'download',
command: this.download,
},
{
separator: true // 分隔符號
},
{
label: 'Delete',
icon: 'delete',
command: this.deleteFile,
},
],
compareData: null,
primaryDragData: [],
secondaryDragData: [],
gridSort: null,
columnType: [
{ name: 'name a-z', code: 'nameAscending'},
{ name: 'name z-a', code: 'nameDescending'},
{ name: 'Dependency a-z', code: 'parentLogAscending'},
{ name: 'Dependency z-a', code: 'parentLogDescending'},
{ name: 'File Type a-z', code: 'fileAscending'},
{ name: 'File Type z-a', code: 'fileDescending'},
{ name: 'Owner a-z', code: 'ownerAscending'},
{ name: 'Owner z-a', code: 'ownerDescending'},
{ name: 'Last Update 遠-近', code: 'updatedAscending'},
{ name: 'Last Update 近-遠', code: 'updatedDescending'},
],
}
},
setup() {
const loginStore = LoginStore();
const store = filesStore();
const allMapDataStore = AllMapDataStore();
const loadingStore = LoadingStore();
const { dependentsData, filesTag } = storeToRefs(store);
const { createFilterId, baseLogId } = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);
return { loginStore, store, dependentsData, filesTag, allMapDataStore, createFilterId, baseLogId, isLoading }
},
components: {
IconDataFormat,
IconRule,
IconsFilter,
IconFlowChart,
IconVector,
IconList,
IconGrid
},
computed: {
/**
* Read allFiles
*/
allFiles: function() {
if(this.store.allFiles.length !== 0){
let sortFiles = Array.from(this.store.allFiles);
sortFiles.sort((x,y) => new Date(y.updated_base) - new Date(x.updated_base));
return sortFiles;
}
},
/**
* 時間排序,如果沒有 accessed_at 就不加入 data
*/
recentlyUsedFiles: function() {
let recentlyUsedFiles = Array.from(this.store.allFiles);
recentlyUsedFiles = recentlyUsedFiles.filter(item => item.accessed_at !== null);
recentlyUsedFiles.sort((x, y) => new Date(y.accessed_base) - new Date(x.accessed_base));
return recentlyUsedFiles;
},
/**
* Compare Submit button disabled
*/
isCompareDisabledButton: function() {
let result = this.primaryDragData.length === 0 || this.secondaryDragData.length === 0 ? true : false;
return result;
}
},
watch: {
filesTag: {
handler(newValue) {
if(newValue !== 'COMPARE'){
this.primaryDragData = [];
this.secondaryDragData = [];
}
}
},
allFiles: {
handler(newValue) {
this.compareData = JSON.parse(JSON.stringify(newValue));
}
}
},
methods: {
/**
* Set Row Style
*/
setRowClass() {
return ['group']
},
/**
* Set Compare Row Style
*/
setCompareRowClass() {
return ['leading-6']
},
/**
* 選擇該 files 進入 Discover/Compare/Design 頁面
* @param {object} file
*/
enterDiscover(file){
let type;
let fileId;
let params;
switch (file.type) {
case 'log':
this.createFilterId = null;
this.baseLogId = file.id;
fileId = file.id;
type = 'log';
params = { type: type, fileId: fileId };
this.$router.push({name: 'Map', params: params});
// this.$router.push({name: 'Map', params: params, query: params});
break;
case 'filter':
this.createFilterId = file.id;
this.baseLogId = file.parent.id;
fileId = file.id;
type = 'filter';
params = { type: type, fileId: fileId };
this.$router.push({name: 'Map', params: params});
// this.$router.push({name: 'Map', params: params, query: params});
break;
// 先不考慮 MAP 只做 CONFORMANCE
case 'log-check':
// path: "/:type/:checkType/:checkId/conformance/:checkFileId"
type = 'rule';
params = { type: type, checkType: 'log', checkId: file.id, checkFileId: file.parent.id };
this.$router.push({name: 'CheckConformance', params: params});
break
case 'filter-check':
type = 'rule';
params = { type: type, checkType: 'filter', checkId: file.id, checkFileId: file.parent.id };
this.$router.push({name: 'CheckConformance', params: params});
break;
}
},
/**
* Right Click DOM Event
* @param {event} event
* @param {string} file
*/
onRightClick(event, file) {
this.selectedType = file.type;
this.selectedId = file.id;
this.selecteName = file.name;
this.$refs.fileRightMenu.show(event)
},
/**
* Right Click Table DOM Event
* @param {event} event
*/
onRightClickTable(event) {
this.selectedType = event.data.type;
this.selectedId = event.data.id;
this.selecteName = event.data.name;
this.$refs.fileRightMenu.show(event.originalEvent)
},
/**
* Right Click Gride Card DOM Event
* @param {event} event
* @param {number} index
*/
onGridCardClick(file, index) {
this.selectedType = file.type;
this.selectedId = file.id;
this.selecteName = file.name;
this.isActive = index;
},
/**
* File's Rename
* @param {string} type
* @param {number} id
* @param {string} source hover icon
*/
rename(type, id, source) {
if(type && id && source === 'list-hover') {
this.selectedType = type;
this.selectedId = id;
}
renameModal(this.store.rename, this.selectedType, this.selectedId);
},
/**
* Delete file
* @param {string} type
* @param {number} id
* @param {string} source hover icon
*/
async deleteFile(type, id, name, source) {
let srt = '';
let data = [];
// 判斷是否來自 hover icon 選單
if(type && id && name && source === 'list-hover') {
this.selectedType = type;
this.selectedId = id;
this.selecteName = name;
}
// 取得相依性檔案
await this.store.getDependents(this.selectedType, this.selectedId);
if(this.dependentsData.length !== 0) {
data = [...this.dependentsData];
data.forEach(i => {
switch (i.type) {
case 'log-check':
i.type = 'rule';
break;
case 'filter-check':
i.type = 'rule';
break;
}
let content = `<li>[${i.type}] ${i.name}</li>`;
srt += content;
});
}
deleteFileModal(srt, this.selectedType, this.selectedId, this.selecteName);
srt = '';
},
/**
* Download file as CSV
* @param {string} type
* @param {number} id
* @param {string} source hover icon
*/
download(type, id, source, name) {
if(type && id && source === 'list-hover' && name) {
this.selectedType = type;
this.selectedId = id;
this.selecteName = name;
}
this.store.downloadFileCSV(this.selectedType, this.selectedId, this.selecteName);
},
/**
* Delete Compare Primary log
*/
primaryDragDelete() {
this.compareData.unshift(this.primaryDragData[0]);
this.primaryDragData.length = 0;
},
/**
* Delete Compare Secondary log
*/
secondaryDragDelete() {
this.compareData.unshift(this.secondaryDragData[0]);
this.secondaryDragData.length = 0;
},
/**
* Enter the Compare page
*/
compareSubmit() {
const primaryType = this.primaryDragData[0].type;
const secondaryType = this.secondaryDragData[0].type;
const primaryId = this.primaryDragData[0].id;
const secondaryId = this.secondaryDragData[0].id;
const params = { primaryType: primaryType, primaryId: primaryId, secondaryType: secondaryType, secondaryId: secondaryId };
this.$router.push({name: 'CompareDashboard', params: params});
},
/**
*
* @param {event} event choose columnType item
*/
getGridSortData(event) {
const code = event.value.code;
// 文字排序: 將 name 字段轉換為小寫進行比較,使用 localeCompare() 方法進行字母順序比較
switch (code) {
case 'nameAscending':
this.compareData = this.compareData.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
break;
case 'nameDescending':
this.compareData = this.compareData.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).reverse();
break;
case 'parentLogAscending':
this.compareData = this.compareData.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase()));
break;
case 'parentLogDescending':
this.compareData = this.compareData.sort((a, b) => a.parentLog.toLowerCase().localeCompare(b.parentLog.toLowerCase())).reverse();
break;
case 'fileAscending':
this.compareData = this.compareData.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase()));
break;
case 'fileDescending':
this.compareData = this.compareData.sort((a, b) => a.fileType.toLowerCase().localeCompare(b.fileType.toLowerCase())).reverse();
break;
case 'ownerAscending':
this.compareData = this.compareData.sort((a, b) => a.ownerName.toLowerCase().localeCompare(b.ownerName.toLowerCase()));
break;
case 'ownerDescending':
this.compareData = this.compareData.sort((a, b) => a.ownerName.toLowerCase().localeCompare(b.ownerName.toLowerCase())).reverse();
break;
case 'updatedAscending':
this.compareData = this.compareData.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base));
break;
case 'updatedDescending':
this.compareData = this.compareData.sort((a, b) => new Date(a.updated_base) - new Date(b.updated_base)).reverse();
break;
}
},
},
mounted() {
this.isLoading = true;
this.store.fetchAllFiles();
window.addEventListener('click', (e) => {
const clickedLi = e.target.closest('li');
if(!clickedLi || !clickedLi.id.startsWith('li')) this.isActive = null;
})
this.isLoading = false;
},
}
</script>
<style scoped>
:deep(thead) {
@apply sticky top-0 bg-neutral-10 after:border-b after:border-neutral-500 after:w-full after:left-0 after:bottom-0 after:absolute
}
:deep(table th) {
@apply border-b !border-neutral-500 !p-2 text-left font-bold !bg-neutral-10 whitespace-nowrap break-keep overflow-hidden text-ellipsis
}
:deep(table td) {
@apply border-b border-neutral-300 !p-2 whitespace-nowrap break-keep overflow-hidden text-ellipsis
}
:deep(tbody > tr) {
@apply duration-300 cursor-pointer hover:bg-primary/10 focus:!outline-none
}
:deep(.p-sortable-column) {
@apply focus:!shadow-none !text-neutral-900
}
:deep(.p-sortable-column-icon) {
@apply !text-[#6c757d]
}
</style>