From f2b02dd10b833c633e1e033ff8b437539ac06358 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Fri, 23 Aug 2024 21:39:22 -0400 Subject: [PATCH] Add userFileStore (#611) * WIP * Refactor * Add userFileStore test --- src/stores/userFileStore.ts | 173 +++++++++++++++++++++ tests-ui/tests/store/userFileStore.test.ts | 169 ++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 src/stores/userFileStore.ts create mode 100644 tests-ui/tests/store/userFileStore.test.ts diff --git a/src/stores/userFileStore.ts b/src/stores/userFileStore.ts new file mode 100644 index 000000000..2cd065713 --- /dev/null +++ b/src/stores/userFileStore.ts @@ -0,0 +1,173 @@ +import { defineStore } from 'pinia' +import { api } from '@/scripts/api' +import type { TreeNode } from 'primevue/treenode' +import { buildTree } from '@/utils/treeUtil' + +interface OpenFile { + path: string + content: string + isModified: boolean + originalContent: string +} + +interface StoreState { + files: string[] + openFiles: OpenFile[] +} + +interface ApiResponse { + success: boolean + data?: T + message?: string +} + +export const useUserFileStore = defineStore('userFile', { + state: (): StoreState => ({ + files: [], + openFiles: [] + }), + + getters: { + getOpenFile: (state) => (path: string) => + state.openFiles.find((file) => file.path === path), + modifiedFiles: (state) => state.openFiles.filter((file) => file.isModified), + workflowsTree: (state): TreeNode => + buildTree(state.files, (path: string) => path.split('/')) + }, + + actions: { + async openFile(path: string): Promise { + if (this.getOpenFile(path)) return + + const { success, data } = await this.getFileData(path) + if (success && data) { + this.openFiles.push({ + path, + content: data, + isModified: false, + originalContent: data + }) + } + }, + + closeFile(path: string): void { + const index = this.openFiles.findIndex((file) => file.path === path) + if (index !== -1) { + this.openFiles.splice(index, 1) + } + }, + + updateFileContent(path: string, newContent: string): void { + const file = this.getOpenFile(path) + if (file) { + file.content = newContent + file.isModified = file.content !== file.originalContent + } + }, + + async saveOpenFile(path: string): Promise { + const file = this.getOpenFile(path) + if (file?.isModified) { + const result = await this.saveFile(path, file.content) + if (result.success) { + file.isModified = false + file.originalContent = file.content + } + return result + } + return { success: true } + }, + + discardChanges(path: string): void { + const file = this.getOpenFile(path) + if (file) { + file.content = file.originalContent + file.isModified = false + } + }, + + async loadFiles(dir: string = './'): Promise { + this.files = (await api.listUserData(dir, true, false)).map( + (filePath: string) => filePath.replaceAll('\\', '/') + ) + + this.openFiles = ( + await Promise.all( + this.openFiles.map(async (openFile) => { + if (!this.files.includes(openFile.path)) return null + + const { success, data } = await this.getFileData(openFile.path) + if (success && data !== openFile.originalContent) { + return { + ...openFile, + content: data, + originalContent: data, + isModified: openFile.content !== data + } + } + + return openFile + }) + ) + ).filter((file): file is OpenFile => file !== null) + }, + + async renameFile(oldPath: string, newPath: string): Promise { + const resp = await api.moveUserData(oldPath, newPath) + if (resp.status !== 200) { + return { success: false, message: resp.statusText } + } + + const openFile = this.openFiles.find((file) => file.path === oldPath) + if (openFile) { + openFile.path = newPath + } + + await this.loadFiles() + return { success: true } + }, + + async deleteFile(path: string): Promise { + const resp = await api.deleteUserData(path) + if (resp.status !== 204) { + return { + success: false, + message: `Error removing user data file '${path}': ${resp.status} ${resp.statusText}` + } + } + + const index = this.openFiles.findIndex((file) => file.path === path) + if (index !== -1) { + this.openFiles.splice(index, 1) + } + + await this.loadFiles() + return { success: true } + }, + + async saveFile(path: string, data: string): Promise { + const resp = await api.storeUserData(path, data, { + stringify: false, + throwOnError: false, + overwrite: true + }) + if (resp.status !== 200) { + return { + success: false, + message: `Error saving user data file '${path}': ${resp.status} ${resp.statusText}` + } + } + + await this.loadFiles() + return { success: true } + }, + + async getFileData(path: string): Promise> { + const resp = await api.getUserData(path) + if (resp.status !== 200) { + return { success: false, message: resp.statusText } + } + return { success: true, data: await resp.json() } + } + } +}) diff --git a/tests-ui/tests/store/userFileStore.test.ts b/tests-ui/tests/store/userFileStore.test.ts new file mode 100644 index 000000000..a45e52d51 --- /dev/null +++ b/tests-ui/tests/store/userFileStore.test.ts @@ -0,0 +1,169 @@ +import { setActivePinia, createPinia } from 'pinia' +import { useUserFileStore } from '@/stores/userFileStore' +import { api } from '@/scripts/api' + +// Mock the api +jest.mock('@/scripts/api', () => ({ + api: { + listUserData: jest.fn(), + moveUserData: jest.fn(), + deleteUserData: jest.fn(), + storeUserData: jest.fn(), + getUserData: jest.fn() + } +})) + +describe('useUserFileStore', () => { + let store: ReturnType + + beforeEach(() => { + setActivePinia(createPinia()) + store = useUserFileStore() + }) + + it('should open a file', async () => { + const mockFileData = { success: true, data: 'file content' } + ;(api.getUserData as jest.Mock).mockResolvedValue({ + status: 200, + json: () => mockFileData.data + }) + + await store.openFile('test.txt') + + expect(store.openFiles).toHaveLength(1) + expect(store.openFiles[0]).toEqual({ + path: 'test.txt', + content: 'file content', + isModified: false, + originalContent: 'file content' + }) + }) + + it('should close a file', () => { + store.openFiles = [ + { + path: 'test.txt', + content: 'content', + isModified: false, + originalContent: 'content' + } + ] + + store.closeFile('test.txt') + + expect(store.openFiles).toHaveLength(0) + }) + + it('should update file content', () => { + store.openFiles = [ + { + path: 'test.txt', + content: 'old content', + isModified: false, + originalContent: 'old content' + } + ] + + store.updateFileContent('test.txt', 'new content') + + expect(store.openFiles[0].content).toBe('new content') + expect(store.openFiles[0].isModified).toBe(true) + }) + + it('should save an open file', async () => { + store.openFiles = [ + { + path: 'test.txt', + content: 'modified content', + isModified: true, + originalContent: 'original content' + } + ] + ;(api.storeUserData as jest.Mock).mockResolvedValue({ status: 200 }) + ;(api.listUserData as jest.Mock).mockResolvedValue(['test.txt']) + ;(api.getUserData as jest.Mock).mockResolvedValue({ + status: 200, + json: () => 'modified content' + }) + + await store.saveOpenFile('test.txt') + + expect(store.openFiles[0].isModified).toBe(false) + expect(store.openFiles[0].originalContent).toBe('modified content') + }) + + it('should discard changes', () => { + store.openFiles = [ + { + path: 'test.txt', + content: 'modified content', + isModified: true, + originalContent: 'original content' + } + ] + + store.discardChanges('test.txt') + + expect(store.openFiles[0].content).toBe('original content') + expect(store.openFiles[0].isModified).toBe(false) + }) + + it('should load files', async () => { + ;(api.listUserData as jest.Mock).mockResolvedValue([ + 'file1.txt', + 'file2.txt' + ]) + + await store.loadFiles() + + expect(store.files).toEqual(['file1.txt', 'file2.txt']) + }) + + it('should rename a file', async () => { + store.openFiles = [ + { + path: 'oldfile.txt', + content: 'content', + isModified: false, + originalContent: 'content' + } + ] + ;(api.moveUserData as jest.Mock).mockResolvedValue({ status: 200 }) + ;(api.listUserData as jest.Mock).mockResolvedValue(['newfile.txt']) + + await store.renameFile('oldfile.txt', 'newfile.txt') + + expect(store.openFiles[0].path).toBe('newfile.txt') + expect(store.files).toEqual(['newfile.txt']) + }) + + it('should delete a file', async () => { + store.openFiles = [ + { + path: 'file.txt', + content: 'content', + isModified: false, + originalContent: 'content' + } + ] + ;(api.deleteUserData as jest.Mock).mockResolvedValue({ status: 204 }) + ;(api.listUserData as jest.Mock).mockResolvedValue([]) + + await store.deleteFile('file.txt') + + expect(store.openFiles).toHaveLength(0) + expect(store.files).toEqual([]) + }) + + it('should get file data', async () => { + const mockFileData = { content: 'file content' } + ;(api.getUserData as jest.Mock).mockResolvedValue({ + status: 200, + json: () => mockFileData + }) + + const result = await store.getFileData('test.txt') + + expect(result).toEqual({ success: true, data: mockFileData }) + }) +})