From 1d956be78370a5c6e5b3202cf6619f2c59fe7537 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Sat, 9 Dec 2023 12:09:19 +0700 Subject: [PATCH 01/17] fix: duplicate name on create with same name --- Services/client/src/stores/socket.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Services/client/src/stores/socket.ts b/Services/client/src/stores/socket.ts index 59d6692..92e2aab 100644 --- a/Services/client/src/stores/socket.ts +++ b/Services/client/src/stores/socket.ts @@ -1,6 +1,5 @@ import { defineStore } from 'pinia' import { reactive } from 'vue' -import { useFileInfoStore } from '@/stores/file-info-data' import { io } from 'socket.io-client' import { useTreeDataStore } from '@/stores/tree-data' import { storeToRefs } from 'pinia' @@ -53,15 +52,23 @@ export const useSocketStore = defineStore('socket', () => { }, ) - currentFolder.value.push({ - pathname: pathname, - name: pathArray[pathArray.length - 1], - status: true, - folder: [], - file: [], - }) + if ( + currentFolder.value.findIndex( + (v) => v.name === pathArray[pathArray.length - 1], + ) === -1 + ) { + currentFolder.value.push({ + pathname: pathname, + name: pathArray[pathArray.length - 1], + status: true, + folder: [], + file: [], + }) + } - listDataFolder.value = currentFolder.value + currentFolder.value.sort((a, b) => { + return a.name.localeCompare(b.name) + }) } }) From fe9345fbbd12f7bb602f6276fd94a22e81e8d1da Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:34:42 +0700 Subject: [PATCH 02/17] chore: adjust and remove unnecessary --- Services/client/src/stores/socket.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Services/client/src/stores/socket.ts b/Services/client/src/stores/socket.ts index 92e2aab..68e580b 100644 --- a/Services/client/src/stores/socket.ts +++ b/Services/client/src/stores/socket.ts @@ -1,5 +1,4 @@ import { defineStore } from 'pinia' -import { reactive } from 'vue' import { io } from 'socket.io-client' import { useTreeDataStore } from '@/stores/tree-data' import { storeToRefs } from 'pinia' @@ -20,15 +19,11 @@ const { updateNewFile, } = useTreeDataStore() -export const state = reactive({ - connected: false, -}) - export const useSocketStore = defineStore('socket', () => { const socket = io('http://localhost:25570') socket.on('connect', () => { - state.connected = true + console.log('SocketIO Connected') }) socket.on('CreateFolder', (dataSocket) => { @@ -86,7 +81,7 @@ export const useSocketStore = defineStore('socket', () => { listDataFolder.value = updateDeleteFolder(listDataFolder.value, pathname) }) - socket?.on('FileDelete', (dataSocket) => { + socket.on('FileDelete', (dataSocket) => { const { pathname } = dataSocket currentFile.value = updateDeleteFile(currentFile.value, pathname) @@ -109,6 +104,6 @@ export const useSocketStore = defineStore('socket', () => { }) socket.on('disconnect', () => { - state.connected = false + console.log('SocketIO Disconnected') }) }) From 9ff9c179bccb51cf173eac1199d534dabc8da242 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Sun, 10 Dec 2023 09:27:26 +0700 Subject: [PATCH 03/17] refactor: new storage store --- Services/client/src/components/PageLayout.vue | 2 + Services/client/src/stores/storage.ts | 217 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 Services/client/src/stores/storage.ts diff --git a/Services/client/src/components/PageLayout.vue b/Services/client/src/components/PageLayout.vue index 9c3ceb0..278c305 100644 --- a/Services/client/src/components/PageLayout.vue +++ b/Services/client/src/components/PageLayout.vue @@ -15,6 +15,7 @@ import GlobalErrorDialog from './GlobalErrorDialog.vue' import SearchBar from '@/modules/01_user/components/SearchBar.vue' import FileDownload from '@/modules/01_user/components/FileDownload.vue' +import useStorage from '@/stores/storage' const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const @@ -23,6 +24,7 @@ const { isSearch } = storeToRefs(useSearchDataStore()) const { data, currentDept, currentPath } = storeToRefs(useTreeDataStore()) const { createFolder, getCabinet, gotoParent, getFolder } = useTreeDataStore() +useStorage() useSocketStore() const viewMode = ref<'view_list' | 'view_module'>('view_list') diff --git a/Services/client/src/stores/storage.ts b/Services/client/src/stores/storage.ts new file mode 100644 index 0000000..3533435 --- /dev/null +++ b/Services/client/src/stores/storage.ts @@ -0,0 +1,217 @@ +import { computed, reactive, ref } from 'vue' +import { defineStore } from 'pinia' +import { io } from 'socket.io-client' +import api from '@/services/HttpService' + +function constructUrl(path: string | string[], append = true) { + const arr = Array.isArray(path) ? path : path.split('/').filter(Boolean) + const url = + import.meta.env.VITE_API_ENDPOINT + + arr.reduce((a, v, i) => { + switch (String(i)) { + case '0': + return `cabinet/${v}` + case '1': + return `${a}/drawer/${v}` + case '2': + return `${a}/folder/${v}` + case '3': + return `${a}/subfolder/${v}` + default: + return a + } + }, '') + return append + ? url + ['cabinet', '/drawer', '/folder', '/subfolder'][arr.length] + : url +} + +const useStorage = defineStore('storage', async () => { + const folderList = ref< + Record< + string, // path that contains folders + { + pathname: string + name: string + }[] + > + >({}) + const fileList = ref< + Record< + string, // path that contains files + { + pathname: string + path: string + fileName: string + fileSize: string + fileType: string + title: string + description: string + category: string[] + keyword: string[] + updatedAt: string + updatedBy: string + createdAt: string + createdBy: string + }[] + > + >({}) + const tree = computed(() => { + type Structure = { + pathname: string + name: string + folder: Structure + file: (typeof fileList.value)[string] + }[] + + let structure: Structure = [] + + // parse list of folder and list of file into tree + Object.entries(folderList.value).forEach(([key, value]) => { + const arr = key.split('/').filter(Boolean) + + // init outer tree + if (arr.length === 0) { + structure = value.map((v) => ({ + pathname: v.pathname, + name: v.name, + folder: [], + file: [], + })) + } else { + let current: Structure[number] | undefined + + // traverse into tree + arr.forEach((v, i) => { + current = + i === 0 + ? structure.find((x) => x.name === v) + : current?.folder.find((x) => x.name === v) + }) + + // set data in tree (object is references to the same object) + if (current) { + current.folder = value.map((v) => ({ + pathname: v.pathname, + name: v.name, + folder: [], + file: [], + })) + current.file = fileList.value[key] ?? [] + } + } + }) + + return structure + }) + const currentInfo = reactive<{ + path: string + dept: number + }>({ + path: '', + dept: 0, + }) + + async function getStorage(path: string = '') { + const res = await api.get<(typeof folderList.value)[string]>( + constructUrl(path), + ) + if (res.status === 200) + folderList.value[path] = res.data.sort((a, b) => + a.pathname.localeCompare(b.pathname), + ) + } + + async function getStorageFile(path: string = '') { + const arr = path.split('/').filter(Boolean) + + if (arr.length < 3) return + + const res = await api.get<(typeof fileList.value)[string]>( + constructUrl(path, false) + '/file', + ) + if (res.status === 200) + fileList.value[path] = res.data.sort((a, b) => + a.pathname.localeCompare(b.pathname), + ) + } + + async function goto(path: string) { + const arr = path.split('/').filter(Boolean) + + for (let i = 0; i < arr.length; i++) { + const current = arr.slice(0, i - arr.length).join('/') + '/' + if (!folderList.value[current]) await getStorage(current) + } + + // only get this path once, after that will get from socket.io-client instead + if (!folderList.value[path]) await getStorage(path) + if (!fileList.value[path]) await getStorageFile(path) + + currentInfo.path = path + currentInfo.dept = path.split('/').filter(Boolean).length - 1 + } + + async function gotoParent() { + const arr = currentInfo.path.split('/').filter(Boolean) + await goto([...arr.slice(0, -1), ''].join('/')) + } + + const socket = io('http://localhost:25570') + + socket.on('connect', () => console.info('Socket.io connected.')) + socket.on('disconnect', () => console.info('Socket.io disconnected.')) + socket.on('CreateFolder', (data: { pathname: string }) => { + const arr = data.pathname.split('/').filter(Boolean) + const path = [...arr.slice(0, -1), ''].join('/') + + if (folderList.value[path]) { + folderList.value[path].push({ + pathname: data.pathname, + name: arr[arr.length - 1], + }) + folderList.value[path].sort((a, b) => + a.pathname.localeCompare(b.pathname), + ) + } + }) + socket.on( + 'EditFolder', + (data: { from: { pathname: string }; to: { pathname: string } }) => { + console.log(data) // TODO: Implement + }, + ) + socket.on('DeleteFolder', (data: { pathname: string }) => { + if (folderList.value[data.pathname]) { + delete folderList.value[data.pathname] + } + + const arr = data.pathname.split('/').filter(Boolean) + const path = [...arr.slice(0, -1), ''].join('/') + + if (folderList.value[path]) { + folderList.value[path] = folderList.value[path].filter( + (v) => v.pathname !== data.pathname, + ) + } + }) + + await goto('เอกสารทดสอบระบบ/dev-test/dev-test/') + console.log(tree.value) + + return { + // information + currentInfo, + folderList, + fileList, + tree, + // fetch + getStorage, + getStorageFile, + // traverse + goto, + gotoParent, + } +}) + +export default useStorage From 5335f9af89cf07bbaa8e494b30f31f92607b4baf Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Sun, 10 Dec 2023 15:00:50 +0700 Subject: [PATCH 04/17] feat: implement socket on edit and fews method --- Services/client/src/stores/storage.ts | 137 ++++++++++++++++++-------- 1 file changed, 95 insertions(+), 42 deletions(-) diff --git a/Services/client/src/stores/storage.ts b/Services/client/src/stores/storage.ts index 3533435..a975cc8 100644 --- a/Services/client/src/stores/storage.ts +++ b/Services/client/src/stores/storage.ts @@ -1,8 +1,11 @@ import { computed, reactive, ref } from 'vue' import { defineStore } from 'pinia' import { io } from 'socket.io-client' + import api from '@/services/HttpService' +import { useLoader } from './loader' + function constructUrl(path: string | string[], append = true) { const arr = Array.isArray(path) ? path : path.split('/').filter(Boolean) const url = @@ -26,8 +29,10 @@ function constructUrl(path: string | string[], append = true) { : url } -const useStorage = defineStore('storage', async () => { - const folderList = ref< +const useStorage = defineStore('storageStore', () => { + const loader = useLoader() + const init = ref(false) + const folder = ref< Record< string, // path that contains folders { @@ -36,7 +41,7 @@ const useStorage = defineStore('storage', async () => { }[] > >({}) - const fileList = ref< + const file = ref< Record< string, // path that contains files { @@ -61,17 +66,18 @@ const useStorage = defineStore('storage', async () => { pathname: string name: string folder: Structure - file: (typeof fileList.value)[string] + file: (typeof file.value)[string] }[] let structure: Structure = [] // parse list of folder and list of file into tree - Object.entries(folderList.value).forEach(([key, value]) => { + Object.entries(folder.value).forEach(([key, value]) => { const arr = key.split('/').filter(Boolean) // init outer tree if (arr.length === 0) { + if (!init.value) init.value = true structure = value.map((v) => ({ pathname: v.pathname, name: v.name, @@ -97,7 +103,7 @@ const useStorage = defineStore('storage', async () => { folder: [], file: [], })) - current.file = fileList.value[key] ?? [] + current.file = file.value[key] ?? [] } } }) @@ -109,15 +115,18 @@ const useStorage = defineStore('storage', async () => { dept: number }>({ path: '', - dept: 0, + dept: 1, }) + if (!init.value) goto() async function getStorage(path: string = '') { - const res = await api.get<(typeof folderList.value)[string]>( - constructUrl(path), - ) - if (res.status === 200) - folderList.value[path] = res.data.sort((a, b) => + const arr = path.split('/').filter(Boolean) + + if (arr.length >= 4) return // this system does not have more than 4 level + + const res = await api.get<(typeof folder.value)[string]>(constructUrl(arr)) + if (res.status === 200 && res.data && Array.isArray(res.data)) + folder.value[path] = res.data.sort((a, b) => a.pathname.localeCompare(b.pathname), ) } @@ -125,31 +134,34 @@ const useStorage = defineStore('storage', async () => { async function getStorageFile(path: string = '') { const arr = path.split('/').filter(Boolean) - if (arr.length < 3) return + if (arr.length < 3) return // file in this system only lives in level 3 and 4 - const res = await api.get<(typeof fileList.value)[string]>( - constructUrl(path, false) + '/file', + const res = await api.get<(typeof file.value)[string]>( + constructUrl(arr, false) + '/file', ) - if (res.status === 200) - fileList.value[path] = res.data.sort((a, b) => + if (res.status === 200 && res.data && Array.isArray(res.data)) + file.value[path] = res.data.sort((a, b) => a.pathname.localeCompare(b.pathname), ) } - async function goto(path: string) { + async function goto(path: string = '', force = false) { + loader.show() const arr = path.split('/').filter(Boolean) + // get all parent to the root structure for (let i = 0; i < arr.length; i++) { const current = arr.slice(0, i - arr.length).join('/') + '/' - if (!folderList.value[current]) await getStorage(current) + if (!folder.value[current] || force) await getStorage(current) } // only get this path once, after that will get from socket.io-client instead - if (!folderList.value[path]) await getStorage(path) - if (!fileList.value[path]) await getStorageFile(path) + if (!folder.value[path] || force) await getStorage(path) + if (!file.value[path] || force) await getStorageFile(path) currentInfo.path = path - currentInfo.dept = path.split('/').filter(Boolean).length - 1 + currentInfo.dept = path.split('/').filter(Boolean).length + loader.hide() } async function gotoParent() { @@ -157,7 +169,8 @@ const useStorage = defineStore('storage', async () => { await goto([...arr.slice(0, -1), ''].join('/')) } - const socket = io('http://localhost:25570') + // socket.io zone + const socket = io('http://localhost:25565') socket.on('connect', () => console.info('Socket.io connected.')) socket.on('disconnect', () => console.info('Socket.io disconnected.')) @@ -165,45 +178,81 @@ const useStorage = defineStore('storage', async () => { const arr = data.pathname.split('/').filter(Boolean) const path = [...arr.slice(0, -1), ''].join('/') - if (folderList.value[path]) { - folderList.value[path].push({ + if (folder.value[path]) { + folder.value[path].push({ pathname: data.pathname, name: arr[arr.length - 1], }) - folderList.value[path].sort((a, b) => - a.pathname.localeCompare(b.pathname), - ) + folder.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname)) + } + }) + // NOTE: + // API planned to make new endpoint that can move and rename in one go. + // Need to change if api handle move and rename file instead of just edit. + socket.on('EditFolder', (data: { from: string; to: string }) => { + const src = data.from.split('/').filter(Boolean) + const dst = data.to.split('/').filter(Boolean) + const path = [...src.slice(0, -1), ''].join('/') + + if (folder.value[path]) { + const val = folder.value[path].find((v) => v.pathname === data.from) + + if (val) { + val.pathname = data.to + val.name = dst[dst.length - 1] + } + } + + for (let key in folder.value) { + if (key.startsWith(data.from)) { + folder.value[key.replace(data.from, data.to)] = folder.value[key].map( + (v) => { + v.pathname = v.pathname.replace(data.from, data.to) + return v + }, + ) + delete folder.value[key] + } } }) - socket.on( - 'EditFolder', - (data: { from: { pathname: string }; to: { pathname: string } }) => { - console.log(data) // TODO: Implement - }, - ) socket.on('DeleteFolder', (data: { pathname: string }) => { - if (folderList.value[data.pathname]) { - delete folderList.value[data.pathname] + for (let key in folder.value) { + if (key.startsWith(data.pathname)) { + delete folder.value[key] + } } const arr = data.pathname.split('/').filter(Boolean) const path = [...arr.slice(0, -1), ''].join('/') - if (folderList.value[path]) { - folderList.value[path] = folderList.value[path].filter( + if (folder.value[path]) { + folder.value[path] = folder.value[path].filter( (v) => v.pathname !== data.pathname, ) } }) - await goto('เอกสารทดสอบระบบ/dev-test/dev-test/') - console.log(tree.value) + async function createFolder(name: string, path: string = currentInfo.path) { + loader.show() + await api.post(constructUrl(path, true), { name }) + loader.hide() + } + async function editFolder(name: string, path: string) { + loader.show() + await api.put(constructUrl(path), { name }) + loader.hide() + } + async function deleteFolder(path: string) { + loader.show() + await api.delete(constructUrl(path)) + loader.hide() + } return { // information currentInfo, - folderList, - fileList, + folder, + file, tree, // fetch getStorage, @@ -211,6 +260,10 @@ const useStorage = defineStore('storage', async () => { // traverse goto, gotoParent, + // operation + createFolder, + editFolder, + deleteFolder, } }) From 0225d6387fbc01ef7c93974357897b42d5b5c3eb Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Sun, 10 Dec 2023 19:33:32 +0700 Subject: [PATCH 05/17] feat: file operation --- Services/client/src/stores/storage.ts | 72 +++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/Services/client/src/stores/storage.ts b/Services/client/src/stores/storage.ts index a975cc8..0cfe8a9 100644 --- a/Services/client/src/stores/storage.ts +++ b/Services/client/src/stores/storage.ts @@ -1,6 +1,7 @@ import { computed, reactive, ref } from 'vue' import { defineStore } from 'pinia' import { io } from 'socket.io-client' +import axios from 'axios' import api from '@/services/HttpService' @@ -117,7 +118,8 @@ const useStorage = defineStore('storageStore', () => { path: '', dept: 1, }) - if (!init.value) goto() + + if (!init.value) goto(sessionStorage.getItem('path') || '') async function getStorage(path: string = '') { const arr = path.split('/').filter(Boolean) @@ -161,6 +163,7 @@ const useStorage = defineStore('storageStore', () => { currentInfo.path = path currentInfo.dept = path.split('/').filter(Boolean).length + sessionStorage.setItem('path', path) loader.hide() } @@ -239,12 +242,72 @@ const useStorage = defineStore('storageStore', () => { } async function editFolder(name: string, path: string) { loader.show() - await api.put(constructUrl(path), { name }) + await api.put(constructUrl(path, false), { name }) loader.hide() } async function deleteFolder(path: string) { loader.show() - await api.delete(constructUrl(path)) + await api.delete(constructUrl(path, false)) + loader.hide() + } + + type FileMetadata = { + title?: string + description?: string + keyword?: string[] + category?: string[] + } + async function createFile( + file: File, + data: FileMetadata, + path: string = currentInfo.path, + ) { + if (path.split('/').filter(Boolean).length < 3) return // the system only allow file to live in level 3 and 4 + + loader.show() + const res = await api.post(constructUrl(path, false) + '/file', { + file: file.name, + ...data, + }) + if (res && res.status === 201 && res.data && res.data.upload) { + await axios + .put(res.data.upload, file, { + headers: { 'Content-Type': file.type }, + onUploadProgress: (e) => console.log(e), + }) + .catch((e) => console.error(e)) + } + loader.hide() + } + async function updateFile(pathname: string, data: FileMetadata, file?: File) { + const arr = pathname.split('/') + + if (arr.length < 4) return // the system only allow file to live in level 3 and 4 + + loader.show() + const res = await api.patch( + constructUrl(arr.slice(0, -1), false) + `/file/${arr[arr.length - 1]}`, + { file: file?.name, ...data }, + ) + if (res && res.status === 201 && res.data && res.data.upload) { + await axios + .put(res.data.upload, file, { + headers: { 'Content-Type': file?.type }, + onUploadProgress: (e) => console.log(e), + }) + .catch((e) => console.error(e)) + } + loader.hide() + } + async function deleteFile(pathname: string) { + const arr = pathname.split('/') + + if (arr.length < 4) return // the system only allow file to live in level 3 and 4 + + loader.show() + await api.patch( + constructUrl(arr.slice(0, -1), false) + `/file/${arr[arr.length - 1]}`, + ) loader.hide() } @@ -264,6 +327,9 @@ const useStorage = defineStore('storageStore', () => { createFolder, editFolder, deleteFolder, + createFile, + updateFile, + deleteFile, } }) From 4e398ab2ad439f70dcad35b4f430d33a48fe4424 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Sun, 10 Dec 2023 20:52:05 +0700 Subject: [PATCH 06/17] fix: only replace from path from start of string --- Services/client/src/stores/storage.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Services/client/src/stores/storage.ts b/Services/client/src/stores/storage.ts index 0cfe8a9..1b58a37 100644 --- a/Services/client/src/stores/storage.ts +++ b/Services/client/src/stores/storage.ts @@ -152,6 +152,7 @@ const useStorage = defineStore('storageStore', () => { const arr = path.split('/').filter(Boolean) // get all parent to the root structure + // this will also triggher init structure as it get root structure for (let i = 0; i < arr.length; i++) { const current = arr.slice(0, i - arr.length).join('/') + '/' if (!folder.value[current] || force) await getStorage(current) @@ -206,17 +207,28 @@ const useStorage = defineStore('storageStore', () => { } } + const regex = new RegExp(`^${data.from}`) + for (let key in folder.value) { if (key.startsWith(data.from)) { - folder.value[key.replace(data.from, data.to)] = folder.value[key].map( + folder.value[key.replace(regex, data.to)] = folder.value[key].map( (v) => { - v.pathname = v.pathname.replace(data.from, data.to) + v.pathname = v.pathname.replace(regex, data.to) return v }, ) delete folder.value[key] } } + for (let key in file.value) { + if (key.startsWith(data.from)) { + file.value[key.replace(regex, data.to)] = file.value[key].map((v) => { + v.pathname = v.pathname.replace(regex, data.to) + return v + }) + delete file.value[key] + } + } }) socket.on('DeleteFolder', (data: { pathname: string }) => { for (let key in folder.value) { From a096a2520baabda11a3f60c2aacf7a56d17bccf9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:10:23 +0700 Subject: [PATCH 07/17] feat: add form wrapper for easier component management --- .../client/src/components/FileFormWrapper.vue | 135 ++++++++++++++++++ .../src/components/FolderFormWrapper.vue | 59 ++++++++ 2 files changed, 194 insertions(+) create mode 100644 Services/client/src/components/FileFormWrapper.vue create mode 100644 Services/client/src/components/FolderFormWrapper.vue diff --git a/Services/client/src/components/FileFormWrapper.vue b/Services/client/src/components/FileFormWrapper.vue new file mode 100644 index 0000000..5ed7262 --- /dev/null +++ b/Services/client/src/components/FileFormWrapper.vue @@ -0,0 +1,135 @@ + + + diff --git a/Services/client/src/components/FolderFormWrapper.vue b/Services/client/src/components/FolderFormWrapper.vue new file mode 100644 index 0000000..5f42442 --- /dev/null +++ b/Services/client/src/components/FolderFormWrapper.vue @@ -0,0 +1,59 @@ + + + From 506a65f27a4fe58afe711281c72f81935468817c Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:10:39 +0700 Subject: [PATCH 08/17] refactor: check keycloak init --- .../client/src/services/KeyCloakService.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Services/client/src/services/KeyCloakService.ts b/Services/client/src/services/KeyCloakService.ts index 8c7ce11..dfa2662 100644 --- a/Services/client/src/services/KeyCloakService.ts +++ b/Services/client/src/services/KeyCloakService.ts @@ -2,15 +2,20 @@ import Keycloak from 'keycloak-js' const keycloak = new Keycloak('/keycloak.json') -export async function login(cb?: (...args: any[]) => void) { - const auth = await keycloak - .init({ - onLoad: 'login-required', - responseMode: 'query', - checkLoginIframe: false, - }) - .catch((e) => console.dir(e)) +let init = false +export async function login(cb?: (...args: any[]) => void) { + const auth = !init + ? await keycloak + .init({ + onLoad: 'login-required', + responseMode: 'query', + checkLoginIframe: false, + }) + .catch((e) => console.dir(e)) + : await keycloak.login().catch((e) => console.dir(e)) + + if (auth) init = true if (auth && cb) cb() } From 52cbf576130531b1c1ce2e816dfd7a4de4451fb2 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:10:45 +0700 Subject: [PATCH 09/17] fix: wrong method --- Services/client/src/stores/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/client/src/stores/storage.ts b/Services/client/src/stores/storage.ts index 1b58a37..4921ece 100644 --- a/Services/client/src/stores/storage.ts +++ b/Services/client/src/stores/storage.ts @@ -317,7 +317,7 @@ const useStorage = defineStore('storageStore', () => { if (arr.length < 4) return // the system only allow file to live in level 3 and 4 loader.show() - await api.patch( + await api.delete( constructUrl(arr.slice(0, -1), false) + `/file/${arr[arr.length - 1]}`, ) loader.hide() From 5a7c56a67ecf1550fb1b6c404d3092226d081db0 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:11:18 +0700 Subject: [PATCH 10/17] refactor: code --- Services/client/src/stores/file-info-data.ts | 139 ++++++------------- 1 file changed, 45 insertions(+), 94 deletions(-) diff --git a/Services/client/src/stores/file-info-data.ts b/Services/client/src/stores/file-info-data.ts index f030f9f..d7b9450 100644 --- a/Services/client/src/stores/file-info-data.ts +++ b/Services/client/src/stores/file-info-data.ts @@ -12,8 +12,8 @@ export interface TypeSetting { } export const useFileInfoStore = defineStore('info', () => { - const fileInfo = ref() const isFilePreview = ref(false) + const fileInfo = ref() const fileIcon: TypeSetting = { word: { icon: 'mdi-file-word-outline', color: 'blue-11' }, excel: { icon: 'mdi-file-excel-outline', color: 'green-4' }, @@ -23,103 +23,55 @@ export const useFileInfoStore = defineStore('info', () => { image: { icon: 'mdi-file-image-outline', color: 'blue-11' }, } const mimeFileMapping: MimeMap = { - 'application/msword': { - ...fileIcon.word, - }, - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': { - ...fileIcon.word, - }, - 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': { - ...fileIcon.word, - }, - 'application/vnd.ms-word.document.macroEnabled.12': { - ...fileIcon.word, - }, - 'application/vnd.ms-word.template.macroEnabled.12': { - ...fileIcon.word, - }, - - 'application/vnd.ms-excel': { - ...fileIcon.excel, - }, - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { - ...fileIcon.excel, - }, - 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': { - ...fileIcon.excel, - }, - 'application/vnd.ms-excel.sheet.macroEnabled.12': { - ...fileIcon.excel, - }, - 'application/vnd.ms-excel.template.macroEnabled.12': { - ...fileIcon.excel, - }, - 'application/vnd.ms-excel.addin.macroEnabled.12': { - ...fileIcon.excel, - }, - 'application/vnd.ms-excel.sheet.binary.macroEnabled.12': { - ...fileIcon.excel, - }, - - 'application/vnd.ms-powerpoint': { - ...fileIcon.powerpoint, - }, + 'application/msword': fileIcon.word, + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + fileIcon.word, + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': + fileIcon.word, + 'application/vnd.ms-word.document.macroEnabled.12': fileIcon.word, + 'application/vnd.ms-word.template.macroEnabled.12': fileIcon.word, + 'application/vnd.ms-excel': fileIcon.excel, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + fileIcon.excel, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': + fileIcon.excel, + 'application/vnd.ms-excel.sheet.macroEnabled.12': fileIcon.excel, + 'application/vnd.ms-excel.template.macroEnabled.12': fileIcon.excel, + 'application/vnd.ms-excel.addin.macroEnabled.12': fileIcon.excel, + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12': fileIcon.excel, + 'application/vnd.ms-powerpoint': fileIcon.powerpoint, 'application/vnd.openxmlformats-officedocument.presentationml.presentation': - { - ...fileIcon.powerpoint, - }, - 'application/vnd.openxmlformats-officedocument.presentationml.template': { - ...fileIcon.powerpoint, - }, - 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': { - ...fileIcon.powerpoint, - }, - 'application/vnd.ms-powerpoint.addin.macroEnabled.12': { - ...fileIcon.powerpoint, - }, - 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': { - ...fileIcon.powerpoint, - }, - 'application/vnd.ms-powerpoint.template.macroEnabled.12': { - ...fileIcon.powerpoint, - }, - 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': { - ...fileIcon.powerpoint, - }, - - 'application/pdf': { - ...fileIcon.pdf, - }, - - 'text/plain': { - ...fileIcon.txt, - }, - - 'image/png': { - ...fileIcon.image, - }, - 'image/jpeg': { - ...fileIcon.image, - }, + fileIcon.powerpoint, + 'application/vnd.openxmlformats-officedocument.presentationml.template': + fileIcon.powerpoint, + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': + fileIcon.powerpoint, + 'application/vnd.ms-powerpoint.addin.macroEnabled.12': fileIcon.powerpoint, + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': + fileIcon.powerpoint, + 'application/vnd.ms-powerpoint.template.macroEnabled.12': + fileIcon.powerpoint, + 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': + fileIcon.powerpoint, + 'application/pdf': fileIcon.pdf, + 'text/plain': fileIcon.txt, + 'image/png': fileIcon.image, + 'image/jpeg': fileIcon.image, } function getType( mimeType: string | undefined, fileName: string | undefined, ): string { - if (mimeType === undefined) { - return 'ไม่ทราบประเภท' + if (mimeType === undefined) return 'ไม่ทราบประเภท' + + const extension = mime.getExtension(mimeType) + + if (extension) return '.' + extension + if (fileName && fileName.includes('.')) { + return fileName.substring(fileName.lastIndexOf('.')) } - const extFomMime = mime.getExtension(mimeType) - if (extFomMime) { - return '.' + extFomMime - } - if (fileName && fileName.includes('.')) { - const dotIndex = fileName.lastIndexOf('.') - const extension = fileName.substring(dotIndex) - return extension - } return 'ไม่ทราบประเภท' } @@ -136,15 +88,14 @@ export const useFileInfoStore = defineStore('info', () => { } function getSize(size: string | undefined): string { - if (size === undefined) { - return 'ไม่ทราบขนาด' - } + if (size === undefined) return 'ไม่ทราบขนาด' + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + let i = 0 let sizeNumber = parseFloat(size) - while (sizeNumber >= 1024 && i < units.length - 1) { + while (sizeNumber >= 1024 && i++ < units.length - 1) { sizeNumber /= 1024 - i++ } return sizeNumber.toFixed(2) + ' ' + units[i] } From 47fe2f8f0a088682573cc94b76523e33b9dbffe7 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:11:32 +0700 Subject: [PATCH 11/17] refactor: code readability --- Services/client/src/components/FileIcon.vue | 43 ++++++++------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/Services/client/src/components/FileIcon.vue b/Services/client/src/components/FileIcon.vue index c8c6957..7c524a5 100644 --- a/Services/client/src/components/FileIcon.vue +++ b/Services/client/src/components/FileIcon.vue @@ -1,6 +1,6 @@ From 8f716e6f44a87322ca63fabc935085651abb990a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:11:41 +0700 Subject: [PATCH 12/17] chore: var name --- Services/client/src/components/FullLoader.vue | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Services/client/src/components/FullLoader.vue b/Services/client/src/components/FullLoader.vue index 3e4bbc4..089a3b6 100644 --- a/Services/client/src/components/FullLoader.vue +++ b/Services/client/src/components/FullLoader.vue @@ -1,22 +1,20 @@ - From 4bdf1f620b01bd6a75d2ccdcaca9d4985a7c2c6f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:51:27 +0700 Subject: [PATCH 13/17] chore: interface and type --- Services/client/src/stores/socket.ts | 109 -------------------------- Services/client/src/stores/storage.ts | 87 ++++++++++---------- 2 files changed, 46 insertions(+), 150 deletions(-) delete mode 100644 Services/client/src/stores/socket.ts diff --git a/Services/client/src/stores/socket.ts b/Services/client/src/stores/socket.ts deleted file mode 100644 index 68e580b..0000000 --- a/Services/client/src/stores/socket.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { defineStore } from 'pinia' -import { io } from 'socket.io-client' -import { useTreeDataStore } from '@/stores/tree-data' -import { storeToRefs } from 'pinia' - -const { - data, - listDataFolder, - currentFolder, - currentPath, - currentFile, - listDataFile, -} = storeToRefs(useTreeDataStore()) -const { - updateEditFolder, - updateDeleteFolder, - updateCreateFolder, - updateDeleteFile, - updateNewFile, -} = useTreeDataStore() - -export const useSocketStore = defineStore('socket', () => { - const socket = io('http://localhost:25570') - - socket.on('connect', () => { - console.log('SocketIO Connected') - }) - - socket.on('CreateFolder', (dataSocket) => { - const { pathname } = dataSocket - - const pathArray: string[] = pathname.split('/').filter(Boolean) - const currentPathResult = pathArray.slice(0, -1).join('/') + '/' - - if (currentPath.value == currentPathResult) { - data.value = updateCreateFolder( - data.value, - pathArray.length, - currentPathResult, - pathname, - { - pathname: pathname, - name: pathArray[pathArray.length - 1], - status: true, - folder: [], - file: [], - }, - ) - - if ( - currentFolder.value.findIndex( - (v) => v.name === pathArray[pathArray.length - 1], - ) === -1 - ) { - currentFolder.value.push({ - pathname: pathname, - name: pathArray[pathArray.length - 1], - status: true, - folder: [], - file: [], - }) - } - - currentFolder.value.sort((a, b) => { - return a.name.localeCompare(b.name) - }) - } - }) - - socket.on('EditFolder', (dataSocket) => { - const { from, to } = dataSocket - data.value = updateEditFolder(data.value, from, to) - currentFolder.value = updateEditFolder(currentFolder.value, from, to) - listDataFolder.value = updateEditFolder(listDataFolder.value, from, to) - }) - - socket.on('DeleteFolder', (dataSocket) => { - const { pathname } = dataSocket - data.value = updateDeleteFolder(data.value, pathname) - currentFolder.value = updateDeleteFolder(currentFolder.value, pathname) - listDataFolder.value = updateDeleteFolder(listDataFolder.value, pathname) - }) - - socket.on('FileDelete', (dataSocket) => { - const { pathname } = dataSocket - - currentFile.value = updateDeleteFile(currentFile.value, pathname) - listDataFile.value = updateDeleteFile(listDataFile.value, pathname) - }) - - socket.on('FileUpdate', (dataSocket) => { - const metadata = dataSocket - - const pathArray: string[] = metadata.pathname.split('/').filter(Boolean) - const currentPathResult = pathArray.slice(0, -1).join('/') + '/' - - if (currentPath.value == currentPathResult) { - listDataFile.value = currentFile.value = updateNewFile( - currentFile.value, - metadata.pathname, - metadata, - ) - } - }) - - socket.on('disconnect', () => { - console.log('SocketIO Disconnected') - }) -}) diff --git a/Services/client/src/stores/storage.ts b/Services/client/src/stores/storage.ts index 4921ece..a2f1e76 100644 --- a/Services/client/src/stores/storage.ts +++ b/Services/client/src/stores/storage.ts @@ -7,6 +7,38 @@ import api from '@/services/HttpService' import { useLoader } from './loader' +type Path = string + +export interface StorageFolder { + pathname: string + name: string + createdAt: string + createdBy: string +} + +export interface StorageFile { + pathname: string + path: string + fileName: string + fileSize: string + fileType: string + title: string + description: string + category: string[] + keyword: string[] + updatedAt: string + updatedBy: string + createdAt: string + createdBy: string +} + +export interface Structure extends StorageFolder { + folder: Structure[] + file: StorageFile[] +} + +type Tree = Structure[] + function constructUrl(path: string | string[], append = true) { const arr = Array.isArray(path) ? path : path.split('/').filter(Boolean) const url = @@ -33,60 +65,29 @@ function constructUrl(path: string | string[], append = true) { const useStorage = defineStore('storageStore', () => { const loader = useLoader() const init = ref(false) - const folder = ref< - Record< - string, // path that contains folders - { - pathname: string - name: string - }[] - > - >({}) - const file = ref< - Record< - string, // path that contains files - { - pathname: string - path: string - fileName: string - fileSize: string - fileType: string - title: string - description: string - category: string[] - keyword: string[] - updatedAt: string - updatedBy: string - createdAt: string - createdBy: string - }[] - > - >({}) + const folder = ref>({}) + const file = ref>({}) const tree = computed(() => { - type Structure = { - pathname: string - name: string - folder: Structure - file: (typeof file.value)[string] - }[] - - let structure: Structure = [] + let structure: Tree = [] // parse list of folder and list of file into tree Object.entries(folder.value).forEach(([key, value]) => { const arr = key.split('/').filter(Boolean) - // init outer tree + // Once run then it is init + if (!init.value) init.value = true + if (arr.length === 0) { - if (!init.value) init.value = true structure = value.map((v) => ({ pathname: v.pathname, name: v.name, + createdAt: v.createdAt, + createdBy: v.createdBy, folder: [], file: [], })) } else { - let current: Structure[number] | undefined + let current: Structure | undefined // traverse into tree arr.forEach((v, i) => { @@ -101,6 +102,8 @@ const useStorage = defineStore('storageStore', () => { current.folder = value.map((v) => ({ pathname: v.pathname, name: v.name, + createdAt: v.createdAt, + createdBy: v.createdBy, folder: [], file: [], })) @@ -116,7 +119,7 @@ const useStorage = defineStore('storageStore', () => { dept: number }>({ path: '', - dept: 1, + dept: 0, }) if (!init.value) goto(sessionStorage.getItem('path') || '') @@ -186,6 +189,8 @@ const useStorage = defineStore('storageStore', () => { folder.value[path].push({ pathname: data.pathname, name: arr[arr.length - 1], + createdAt: 'n/a', + createdBy: 'n/a', }) folder.value[path].sort((a, b) => a.pathname.localeCompare(b.pathname)) } From cc87de5995214b90f60f297695d4dcb56680a8b2 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:51:59 +0700 Subject: [PATCH 14/17] refactor: migrate to storage refactor: migrate to storage --- .../client/src/components/FileFormWrapper.vue | 1 + Services/client/src/components/FileItem.vue | 394 +++++---------- .../client/src/components/FileSearched.vue | 180 ++----- Services/client/src/components/ListView.vue | 451 +++++------------- Services/client/src/components/PageLayout.vue | 129 ++--- Services/client/src/components/Profile.vue | 24 +- .../client/src/components/TreeExplorer.vue | 25 +- .../01_user/components/FileDownload.vue | 4 +- .../modules/01_user/components/SearchBar.vue | 4 +- Services/client/src/stores/file-info-data.ts | 10 +- Services/client/src/stores/searched-data.ts | 18 +- Services/client/src/views/MainLayout.vue | 27 +- 12 files changed, 388 insertions(+), 879 deletions(-) diff --git a/Services/client/src/components/FileFormWrapper.vue b/Services/client/src/components/FileFormWrapper.vue index 5ed7262..7b3dc92 100644 --- a/Services/client/src/components/FileFormWrapper.vue +++ b/Services/client/src/components/FileFormWrapper.vue @@ -3,6 +3,7 @@ import { ref } from 'vue' import { storeToRefs } from 'pinia' import useStorage from '@/stores/storage' import FileForm from './FileForm.vue' +import UploadExistDialog from './UploadExistDialog.vue' const storageStore = useStorage() const { file, currentInfo } = storeToRefs(storageStore) diff --git a/Services/client/src/components/FileItem.vue b/Services/client/src/components/FileItem.vue index 09ed4f7..0a14030 100644 --- a/Services/client/src/components/FileItem.vue +++ b/Services/client/src/components/FileItem.vue @@ -2,233 +2,109 @@ import { computed, ref } from 'vue' import { storeToRefs } from 'pinia' -import FileIcon from '@/components/FileIcon.vue' -import FileItemAction from '@/components/FileItemAction.vue' -import DialogDelete from '@/components/DialogDelete.vue' -import FileForm from './FileForm.vue' -import FolderForm from './FolderForm.vue' -import UploadExistDialog from './UploadExistDialog.vue' -import { useTreeDataStore } from '@/stores/tree-data' +import FileIcon from './FileIcon.vue' +import FileItemAction from './FileItemAction.vue' +import DialogDelete from './DialogDelete.vue' +import FileFormWrapper from './FileFormWrapper.vue' +import FolderFormWrapper from './FolderFormWrapper.vue' + import { useFileInfoStore } from '@/stores/file-info-data' +import useStorage from '@/stores/storage' + +const TREE_LEVEL_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const const props = withDefaults( defineProps<{ action: boolean; viewMode: 'view_list' | 'view_module' }>(), - { - action: false, - }, + { action: false }, ) -const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const const { getFileInfo } = useFileInfoStore() -const { currentFolder, currentFile, currentDept, currentPath } = - storeToRefs(useTreeDataStore()) -const { - createFolder, - editFolder, - getFolder, - deleteFolder, - uploadFile, - updateFile, - deleteFile, - checkFile, - checkFileName, - refaceFile, -} = useTreeDataStore() + +const storageStore = useStorage() +const { folder, file, currentInfo } = storeToRefs(storageStore) +const { goto, deleteFolder, deleteFile } = storageStore + +const fileFormComponent = ref>() +const folderFormComponent = ref>() + +const deleteState = ref(false) +const deletePath = ref('') +const deleteTarget = ref<'deleteFolder' | 'deleteFile'>() +const deleteMap = { deleteFolder, deleteFile } const currentIcon = computed(() => - currentDept.value === 0 + currentInfo.value.dept === 0 ? 'mdi-file-cabinet' - : currentDept.value === 1 + : currentInfo.value.dept === 1 ? 'o_inbox' : 'o_folder_open', ) -const dialogDeleteState = ref(false) -const deleteFormPath = ref('') -const deleteFormType = ref<'deleteFolder' | 'deleteFile'>() - -const folderFormState = ref(false) -const folderFormPath = ref('') -const folderFormData = ref<{ - name?: string -}>({}) -const folderFormType = ref<'edit' | 'create'>('create') -const fileFormState = ref(false) -const fileFormPath = ref('') -const fileFormData = ref<{ - file?: File - title?: string - description?: string - keyword?: string[] - category?: string[] -}>({}) -const fileFormType = ref<'edit' | 'create'>('create') -const fileFormError = ref<{ fileExist?: boolean; fileName2Long?: boolean }>({}) -const fileExistNotification = ref(false) -const fileFormComponent = ref>() - function triggerFolderDelete(pathname: string) { - deleteFormType.value = 'deleteFolder' - deleteFormPath.value = pathname - dialogDeleteState.value = !dialogDeleteState.value + deleteTarget.value = 'deleteFolder' + deletePath.value = pathname + deleteState.value = !deleteState.value } function triggerFileDelete(pathname: string) { - deleteFormType.value = 'deleteFile' - deleteFormPath.value = pathname - dialogDeleteState.value = !dialogDeleteState.value -} - -function triggerFolderCreate() { - folderFormType.value = 'create' - folderFormData.value = {} - folderFormState.value = !folderFormState.value -} - -function triggerFolderEdit(name: string, pathname: string) { - folderFormType.value = 'edit' - folderFormPath.value = pathname - folderFormData.value.name = name - folderFormState.value = true -} - -async function submitFolderForm(value: { - mode: 'create' | 'edit' - name: string -}) { - if (value.mode === 'create') { - await createFolder(value.name) - } else { - await editFolder(value.name, folderFormPath.value) - } -} - -function triggerFileCreate() { - fileFormType.value = 'create' - fileFormData.value = {} - fileFormState.value = !fileFormState.value -} - -function triggerFileEdit( - value: { - title: string - description: string - keyword: string[] - category: string[] - }, - pathname: string, -) { - fileFormState.value = true - fileFormType.value = 'edit' - fileFormPath.value = pathname - fileFormData.value = { - title: value.title, - description: value.description, - keyword: value.keyword, - category: value.category, - } -} - -const currentParam = ref[0]>() - -async function submitFileForm( - value: { - mode: 'create' | 'edit' - file?: File - title: string - description: string - keyword: string[] - category: string[] - }, - force = false, -) { - currentParam.value = value - - if (value.file && checkFile(value.file.name) && !force) { - fileExistNotification.value = true - return - } - - if (value.mode === 'create' && value.file) { - await uploadFile(currentPath.value, value.file, { - title: value.title, - description: value.description, - keyword: value.keyword, - category: value.category, - }) - setTimeout(() => { - refaceFile(currentPath.value) - }, 3000) - - setTimeout(() => { - refaceFile(currentPath.value) - }, 10000) - } else { - await updateFile( - fileFormPath.value, - { - title: value.title, - description: value.description, - keyword: value.keyword, - category: value.category, - }, - value.file, - ) - setTimeout(() => { - refaceFile(currentPath.value) - }, 3000) - - setTimeout(() => { - refaceFile(currentPath.value) - }, 10000) - } - fileFormData.value = {} - fileFormState.value = false - currentParam.value = undefined - fileFormComponent.value?.reset() + deleteTarget.value = 'deleteFile' + deletePath.value = pathname + deleteState.value = !deleteState.value }