From 22f9a2d371e24ac7b728b6a134a77083aca3dddb Mon Sep 17 00:00:00 2001 From: k-y0u Date: Wed, 8 Nov 2023 12:00:39 +0100 Subject: [PATCH 1/8] Rename FileExplorerActionCard to FileExplorerActionButton --- src/components/FileExplorer.vue | 11 +++++------ ...rerActionCard.vue => FileExplorerActionButton.vue} | 1 + ...nCard.spec.js => FileExplorerActionButton.spec.js} | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/components/{card/FileExplorerActionCard.vue => FileExplorerActionButton.vue} (93%) rename tests/unit/components/{card/FileExplorerActionCard.spec.js => FileExplorerActionButton.spec.js} (82%) diff --git a/src/components/FileExplorer.vue b/src/components/FileExplorer.vue index ded2cd64c..d1ff0edbd 100644 --- a/src/components/FileExplorer.vue +++ b/src/components/FileExplorer.vue @@ -42,11 +42,10 @@
-
@@ -64,7 +63,7 @@ import { toRef, } from 'vue'; import GitEvent from 'src/composables/events/GitEvent'; -import FileExplorerActionCard from 'src/components/card/FileExplorerActionCard.vue'; +import FileExplorerActionButton from 'src/components/FileExplorerActionButton.vue'; import FileName from 'src/components/FileName.vue'; import { getTree, updateFileInformation } from 'src/composables/FileExplorer'; import { getProjectFiles, getStatus } from 'src/composables/Project'; @@ -411,12 +410,12 @@ onUnmounted(() => { background: rgba($primary, 0.1); border-radius: 4px; - .file-explorer-button { + .file-explorer-action-button { display: inline-block } } } -.file-explorer-button { +.file-explorer-action-button { display: none } diff --git a/src/components/card/FileExplorerActionCard.vue b/src/components/FileExplorerActionButton.vue similarity index 93% rename from src/components/card/FileExplorerActionCard.vue rename to src/components/FileExplorerActionButton.vue index 6e4d8f1d8..17b42c7a0 100644 --- a/src/components/card/FileExplorerActionCard.vue +++ b/src/components/FileExplorerActionButton.vue @@ -8,6 +8,7 @@ :class="['tree-action-button', { 'inline-block' : isActionMenuOpen }]" color="primary" icon="fa-solid fa-ellipsis-vertical" + :data-cy="`${file.isFolder ? 'folder': 'file'}-button_${file.id}`" @click.stop="isActionMenuOpen = true" > { +describe('Test component: FileExplorerActionButton', () => { let wrapper; beforeEach(() => { - wrapper = shallowMount(FileExplorerActionCard, { + wrapper = shallowMount(FileExplorerActionButton, { props: { file: { id: 'test' }, projectName: 'projectName', From 20f8fe9b124e9f52105db3bae7443e96e7fa40df Mon Sep 17 00:00:00 2001 From: k-y0u Date: Wed, 15 Nov 2023 15:14:53 +0100 Subject: [PATCH 2/8] Add translations --- src/i18n/en-US/index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js index 2eebefd50..177bc7921 100644 --- a/src/i18n/en-US/index.js +++ b/src/i18n/en-US/index.js @@ -17,6 +17,7 @@ export default { cancel: 'Cancel', search: 'Search', modify: 'Modify', + rename: 'Rename', ok: 'Ok', }, home: { @@ -138,11 +139,13 @@ export default { addFile: 'Add', file: { create: 'File is created 🥳!', + rename: 'File is renamed 🥳!', delete: 'File is deleted.', add: 'File is added 🥳!', }, folder: { create: 'Folder is created 🥳!', + rename: 'Folder is renamed 🥳!', delete: 'Folder is deleted.', }, }, @@ -192,10 +195,12 @@ export default { default: { folder: { create: 'An error occured while creating folder.', + rename: 'An error occured while renaming folder.', delete: 'An error occured while deleting folder.', }, file: { create: 'An error occured while creating file.', + rename: 'An error occured while renaming file.', delete: 'An error occured while deleting file.', add: 'An error occured while adding file.', }, @@ -416,6 +421,16 @@ export default { confirmDelete: 'Confirm deletion of folder and all its content', }, }, + rename: { + file: { + title: 'Rename file', + input: 'New file name', + }, + folder: { + title: 'Rename folder', + input: 'New folder name', + }, + }, filterParsableFiles: 'Show parsable files', }, }, From 9ab19561063be12b9025bb97957ecda7dbba5ad3 Mon Sep 17 00:00:00 2001 From: k-y0u Date: Thu, 16 Nov 2023 11:14:25 +0100 Subject: [PATCH 3/8] Add RenameFileEvent in FileEvent --- src/composables/events/FileEvent.js | 7 +++++++ tests/unit/composables/events/FileEvent.spec.js | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/composables/events/FileEvent.js b/src/composables/events/FileEvent.js index e906be58b..36baff539 100644 --- a/src/composables/events/FileEvent.js +++ b/src/composables/events/FileEvent.js @@ -7,6 +7,12 @@ import { Subject } from 'rxjs'; */ const CreateFileEvent = new Subject(); +/** + * Represent a rxjs Event object to emit and to receive events about file name modification. + * @typedef {Subject} RenameFileEvent + */ +const RenameFileEvent = new Subject(); + /** * Represent a rxjs Event object to emit and to receive events about node deletion. * The event should contain the deleted node Object from the tree. @@ -50,6 +56,7 @@ const UpdateEditorContentEvent = new Subject(); export default { CreateFileEvent, + RenameFileEvent, DeleteFileEvent, GlobalUploadFilesEvent, SelectFileTabEvent, diff --git a/tests/unit/composables/events/FileEvent.spec.js b/tests/unit/composables/events/FileEvent.spec.js index 676ec4b55..6c5e77cf7 100644 --- a/tests/unit/composables/events/FileEvent.spec.js +++ b/tests/unit/composables/events/FileEvent.spec.js @@ -23,6 +23,13 @@ describe('Test composable: FileEvent', () => { }); }); + describe('Test event: RenameFileEvent', () => { + it('should export a Subject', () => { + expect(FileEvent.RenameFileEvent).toBeDefined(); + expect(FileEvent.RenameFileEvent).toEqual(new Subject()); + }); + }); + describe('Test event: DeleteFileEvent', () => { it('should export a Subject', () => { expect(FileEvent.DeleteFileEvent).toBeDefined(); From 79c887db59e059472ee3c6f687b95ac3b953fb39 Mon Sep 17 00:00:00 2001 From: k-y0u Date: Thu, 16 Nov 2023 10:56:34 +0100 Subject: [PATCH 4/8] Create RenameFileDialog & RenameFileForm --- src/components/dialog/RenameFileDialog.vue | 65 +++++++++ src/components/form/RenameFileForm.vue | 132 ++++++++++++++++++ .../dialog/RenameFileDialog.spec.js | 83 +++++++++++ .../components/form/RenameFileForm.spec.js | 114 +++++++++++++++ 4 files changed, 394 insertions(+) create mode 100644 src/components/dialog/RenameFileDialog.vue create mode 100644 src/components/form/RenameFileForm.vue create mode 100644 tests/unit/components/dialog/RenameFileDialog.spec.js create mode 100644 tests/unit/components/form/RenameFileForm.spec.js diff --git a/src/components/dialog/RenameFileDialog.vue b/src/components/dialog/RenameFileDialog.vue new file mode 100644 index 000000000..13e358a6e --- /dev/null +++ b/src/components/dialog/RenameFileDialog.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/form/RenameFileForm.vue b/src/components/form/RenameFileForm.vue new file mode 100644 index 000000000..a1ca844a5 --- /dev/null +++ b/src/components/form/RenameFileForm.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/tests/unit/components/dialog/RenameFileDialog.spec.js b/tests/unit/components/dialog/RenameFileDialog.spec.js new file mode 100644 index 000000000..64a21cb16 --- /dev/null +++ b/tests/unit/components/dialog/RenameFileDialog.spec.js @@ -0,0 +1,83 @@ +import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-jest'; +import { shallowMount } from '@vue/test-utils'; +import { createI18n } from 'vue-i18n'; +import i18nConfiguration from 'src/i18n'; +import RenameFileDialog from 'src/components/dialog/RenameFileDialog'; +import DialogEvent from 'src/composables/events/DialogEvent'; + +installQuasarPlugin(); + +jest.mock('src/composables/events/DialogEvent', () => ({ + subscribe: jest.fn(), +})); + +describe('Test component: RenameFileDialog', () => { + let wrapper; + let subscribe; + let unsubscribe; + + beforeEach(() => { + subscribe = jest.fn(); + unsubscribe = jest.fn(); + + DialogEvent.subscribe.mockImplementation(() => { + subscribe(); + return { unsubscribe }; + }); + + wrapper = shallowMount(RenameFileDialog, { + props: { + projectName: 'projectName', + }, + global: { + plugins: [ + createI18n({ locale: 'en-US', messages: i18nConfiguration }), + ], + }, + }); + }); + + describe('Test variables initialization', () => { + describe('Test props: projectName', () => { + it('should match "projectName"', () => { + expect(wrapper.vm.projectName).toEqual('projectName'); + }); + }); + + describe('Test props: fileToRename', () => { + it('should match "fileToRename"', () => { + expect(wrapper.vm.fileToRename).toEqual(null); + }); + }); + }); + + describe('Test function: setRenamedFile', () => { + const file = { id: 'test', isFolder: true }; + + it('should set newFile and onFolder on valid event key', () => { + expect(wrapper.vm.fileToRename).toBeNull(); + + wrapper.vm.setRenamedFile({ key: 'RenameFile', file }); + expect(wrapper.vm.fileToRename).toEqual(file); + }); + + it('should not set newFile and onFolder on invalid event key', () => { + wrapper.vm.setRenamedFile({ key: 'NotRenameFile', file }); + expect(wrapper.vm.fileToRename).toBeNull(); + }); + }); + + describe('Test hook function: onMounted', () => { + it('should subscribe DialogEvent', () => { + expect(subscribe).toHaveBeenCalledTimes(1); + }); + }); + + describe('Test hook function: onUnmounted', () => { + it('should unsubscribe DialogEvent', () => { + expect(unsubscribe).toHaveBeenCalledTimes(0); + wrapper.unmount(); + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/tests/unit/components/form/RenameFileForm.spec.js b/tests/unit/components/form/RenameFileForm.spec.js new file mode 100644 index 000000000..6faa14250 --- /dev/null +++ b/tests/unit/components/form/RenameFileForm.spec.js @@ -0,0 +1,114 @@ +import { installQuasarPlugin } from '@quasar/quasar-app-extension-testing-unit-jest'; +import { shallowMount } from '@vue/test-utils'; +import RenameFileForm from 'src/components/form/RenameFileForm'; +import { Notify } from 'quasar'; + +installQuasarPlugin({ + plugins: [Notify], +}); + +jest.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (t) => t, + }), +})); + +jest.mock('src/composables/Project', () => ({ + gitRemove: jest.fn(() => Promise.resolve()), + readDir: jest.fn(() => ['test']), + rename: jest.fn( + (old, rename) => (rename === 'projectName/rename' ? Promise.resolve() : Promise.reject()), + ), +})); + +describe('Test component: RenameFileForm', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(RenameFileForm, { + props: { + projectName: 'projectName', + file: { + id: 'test', + isfolder: false, + label: 'test', + }, + }, + }); + }); + + describe('Test variables initialization', () => { + describe('Test props: projectName', () => { + it('should match "projectName"', () => { + expect(wrapper.vm.projectName).toEqual('projectName'); + }); + }); + + describe('Test props: file', () => { + it('should match file', () => { + expect(wrapper.vm.file).toEqual({ + id: 'test', + isfolder: false, + label: 'test', + }); + }); + }); + + describe('Test variable: fileName', () => { + it('should match the original file label', () => { + expect(wrapper.vm.fileName).toEqual('test'); + }); + }); + + describe('Test variable: submitting', () => { + it('should be false', () => { + expect(wrapper.vm.submitting).toEqual(false); + }); + }); + + describe('Test variable: labels', () => { + it('should match []', () => { + expect(wrapper.vm.labels).toEqual([]); + }); + }); + }); + + describe('Test function: onSubmit', () => { + it('should emit an event and a positive notification on success', async () => { + wrapper.vm.fileName = 'rename'; + Notify.create = jest.fn(); + + await wrapper.vm.onSubmit(); + + expect(wrapper.emitted()['file:rename']).toBeTruthy(); + expect(Notify.create).toHaveBeenCalledWith(expect.objectContaining({ type: 'positive' })); + }); + + it('should emit a negative notification on error', async () => { + wrapper.vm.fileName = 'wrong'; + Notify.create = jest.fn(); + + await wrapper.vm.onSubmit(); + + expect(Notify.create).toHaveBeenCalledWith(expect.objectContaining({ type: 'negative' })); + }); + + it('should emit nothing when rename with the same label', async () => { + Notify.create = jest.fn(); + + await wrapper.vm.onSubmit(); + + expect(Notify.create).not.toHaveBeenCalled(); + }); + }); + + describe('Test function: initLabels', () => { + it('should set a new array to "labels" variable', async () => { + wrapper.vm.labels = ['test']; + + await wrapper.vm.initLabels(); + + expect(wrapper.vm.labels).toEqual([]); + }); + }); +}); From 398a6a37a8b3d73ba7d14c65441f9c3335df033a Mon Sep 17 00:00:00 2001 From: k-y0u Date: Thu, 16 Nov 2023 16:04:31 +0100 Subject: [PATCH 5/8] Add 'Rename' action in FileExplorer file/folder menu --- src/components/FileExplorer.vue | 10 ++++- .../menu/FileExplorerActionMenu.vue | 29 ++++++++++++++ src/components/tab/FileTabs.vue | 22 +++++++++++ src/layouts/ModelizerTextLayout.vue | 10 +++-- tests/unit/components/FileExplorer.spec.js | 19 +++++++++ .../menu/FileExplorerActionMenu.spec.js | 17 ++++++++ tests/unit/components/tab/FileTabs.spec.js | 39 +++++++++++++++++++ 7 files changed, 140 insertions(+), 6 deletions(-) diff --git a/src/components/FileExplorer.vue b/src/components/FileExplorer.vue index d1ff0edbd..dedc09d29 100644 --- a/src/components/FileExplorer.vue +++ b/src/components/FileExplorer.vue @@ -95,6 +95,7 @@ const filterTrigger = ref(toRef(props, 'showParsableFiles').value.toString()); let selectFileTabSubscription; let createFileSubscription; +let renameFileSubscription; let deleteFileSubscription; let updateEditorContentSubscription; let addRemoteSubscription; @@ -327,9 +328,10 @@ async function openModelFiles() { /** * Update localFileInformations and nodes of the tree then update all status. * Also, expand model folders and open parsable files. + * @param {boolean} avoidOpening - Avoid automatic model file(s) opening. * @returns {Promise} Promise with nothing on success otherwise an error. */ -async function initTreeNodes() { +async function initTreeNodes(avoidOpening = false) { localFileInformations.value = await getProjectFiles(props.projectName); nodes.value = getTree(props.projectName, localFileInformations.value); @@ -337,7 +339,7 @@ async function initTreeNodes() { await updateAllFilesStatus(); // TODO: Find a better way to stub it on shallowMount. - if (fileExplorerRef.value.getNodeByKey) { + if (fileExplorerRef.value.getNodeByKey && !avoidOpening) { openModelFiles(); } } @@ -349,6 +351,9 @@ onMounted(async () => { createFileSubscription = FileEvent.CreateFileEvent.subscribe((event) => { onCreateFile(event); }); + renameFileSubscription = FileEvent.RenameFileEvent.subscribe(() => { + initTreeNodes(true); + }); deleteFileSubscription = FileEvent.DeleteFileEvent.subscribe(onDeleteFile); updateEditorContentSubscription = FileEvent.UpdateEditorContentEvent.subscribe((event) => { updateFileStatus(event); @@ -378,6 +383,7 @@ onMounted(async () => { onUnmounted(() => { selectFileTabSubscription.unsubscribe(); createFileSubscription.unsubscribe(); + renameFileSubscription.unsubscribe(); deleteFileSubscription.unsubscribe(); updateEditorContentSubscription.unsubscribe(); addRemoteSubscription.unsubscribe(); diff --git a/src/components/menu/FileExplorerActionMenu.vue b/src/components/menu/FileExplorerActionMenu.vue index 218d58c5e..4e95816a4 100644 --- a/src/components/menu/FileExplorerActionMenu.vue +++ b/src/components/menu/FileExplorerActionMenu.vue @@ -6,6 +6,23 @@ @hide="emit('hide:menu')" > + + + + + + {{ $t('actions.default.rename') }} + +