From 3fc847cde80ce586b330895dd1c4bace59b00712 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Sun, 4 Aug 2024 20:18:21 -0400 Subject: [PATCH] Make userFileStore handle opened files (#300) --- src/stores/userFileStore.ts | 114 ++++++++++++- tests-ui/tests/stores/userFileStore.test.ts | 169 ++++++++++++++++++++ 2 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 tests-ui/tests/stores/userFileStore.test.ts diff --git a/src/stores/userFileStore.ts b/src/stores/userFileStore.ts index ed1ad3929..252844b56 100644 --- a/src/stores/userFileStore.ts +++ b/src/stores/userFileStore.ts @@ -3,12 +3,26 @@ import { api } from '@/scripts/api' import { TreeNode } from 'primevue/treenode' import { defaultGraph } from '@/scripts/defaultGraph' +export interface OpenFile { + path: string + content: string + isModified: boolean + originalContent: string +} + export const useUserFileStore = defineStore('userFile', { state: () => ({ - files: [] as string[] + files: [] as string[], + openFiles: [] as OpenFile[] }), getters: { - workflowsTree(): TreeNode { + getOpenFile: (state) => (path: string) => { + return state.openFiles.find((file) => file.path === path) + }, + modifiedFiles: (state) => { + return state.openFiles.filter((file) => file.isModified) + }, + workflowsTree(state): TreeNode { const rootPath = 'workflows' const root: TreeNode = { key: rootPath, @@ -18,7 +32,7 @@ export const useUserFileStore = defineStore('userFile', { children: [] } - for (const filePath of this.files) { + for (const filePath of state.files) { const parts = filePath.split('/') let current = root let key = rootPath @@ -48,16 +62,99 @@ export const useUserFileStore = defineStore('userFile', { } }, actions: { + async openFile(path: string) { + if (this.getOpenFile(path)) return + + const { success, data } = await this.getFileData(path) + if (success) { + this.openFiles.push({ + path, + content: data, + isModified: false, + originalContent: data + }) + } + }, + closeFile(path: string) { + const index = this.openFiles.findIndex( + (file: OpenFile) => file.path === path + ) + if (index !== -1) { + this.openFiles.splice(index, 1) + } + }, + updateFileContent(path: string, newContent: string) { + const file = this.getOpenFile(path) + if (file) { + file.content = newContent + file.isModified = file.content !== file.originalContent + } + }, + async saveOpenFile(path: string) { + const file = this.getOpenFile(path) + console.error(file) + if (file && file.isModified) { + const result = await this.saveFile(path, file.content) + console.error(result) + if (result.success) { + file.isModified = false + file.originalContent = file.content + } + return result + } + return { success: true } + }, + discardChanges(path: string) { + const file = this.getOpenFile(path) + if (file) { + file.content = file.originalContent + file.isModified = false + } + }, async loadFiles(dir: string = 'workflows') { this.files = (await api.listUserData(dir, true, false)).map( (filePath: string) => filePath.replaceAll('\\', '/') ) + + // Update openFiles to reflect changes in the file system + this.openFiles = await Promise.all( + this.openFiles.map(async (openFile) => { + if (!this.files.includes(openFile.path)) { + // File has been deleted from the file system + return null + } + + // Check if file content has changed + const { success, data } = await this.getFileData(openFile.path) + if (success && data !== openFile.originalContent) { + // File has been modified in the file system + return { + ...openFile, + content: data, + originalContent: data, + isModified: openFile.content !== data + } + } + + return openFile + }) + ) + + // Remove null entries (deleted files) + this.openFiles = this.openFiles.filter((file) => file !== null) }, async renameFile(oldPath: string, newPath: string) { const resp = await api.moveUserData(oldPath, newPath) if (resp.status !== 200) { return { success: false, message: resp.statusText } } + // Update openFiles state + const openFile = this.openFiles.find( + (file: OpenFile) => file.path === oldPath + ) + if (openFile) { + openFile.path = newPath + } await this.loadFiles() return { success: true } }, @@ -69,13 +166,16 @@ export const useUserFileStore = defineStore('userFile', { message: `Error removing user data file '${path}': ${resp.status} ${resp.statusText}` } } + // Remove from openFiles if it's open + const index = this.openFiles.findIndex( + (file: OpenFile) => file.path === path + ) + if (index !== -1) { + this.openFiles.splice(index, 1) + } await this.loadFiles() return { success: true } }, - async moveFile(sourcePath: string, destPath: string) { - await api.moveUserData(sourcePath, destPath) - await this.loadFiles() - }, async saveFile(path: string, data: string) { const resp = await api.storeUserData(path, data, { stringify: false, diff --git a/tests-ui/tests/stores/userFileStore.test.ts b/tests-ui/tests/stores/userFileStore.test.ts new file mode 100644 index 000000000..a45e52d51 --- /dev/null +++ b/tests-ui/tests/stores/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 }) + }) +})