diff --git a/ui/src/components/FormValidation.vue b/ui/src/components/FormValidation.vue new file mode 100644 index 000000000..b4e8daea9 --- /dev/null +++ b/ui/src/components/FormValidation.vue @@ -0,0 +1,46 @@ + + + + + \ No newline at end of file diff --git a/ui/src/components/ViewEditor.vue b/ui/src/components/ViewEditor.vue index 09ff5f463..4070f35de 100644 --- a/ui/src/components/ViewEditor.vue +++ b/ui/src/components/ViewEditor.vue @@ -16,11 +16,12 @@ disabled v-model="srcProject" /> - + + > +
@@ -35,11 +36,12 @@ disabled v-model="srcFolder" /> - + + +
@@ -54,22 +56,29 @@ disabled v-model="srcTable" /> - + + +
- + + +
@@ -90,11 +99,13 @@ disabled v-model="vwProject" /> - + + +
@@ -102,12 +113,15 @@ Folder:
- + + +
@@ -115,34 +129,28 @@ Table:
- + + +
+
+ +
-
- -
@@ -151,18 +159,21 @@ import { getProjects, getTableVariables, getProject } from "@/api/api"; import { getRestructuredProject, getTablesFromListOfFiles, + isEmpty, } from "@/helpers/utils"; import { Project } from "@/types/api"; import { StringArray, ViewEditorData } from "@/types/types"; import { PropType, Ref, defineComponent, onMounted, ref } from "vue"; import VariableSelector from "@/components/VariableSelector.vue"; import Dropdown from "@/components/Dropdown.vue"; +import FormValidation from "@/components/FormValidation.vue"; export default defineComponent({ name: "ViewEditor", components: { VariableSelector, Dropdown, + FormValidation }, props: { sourceFolder: String, @@ -203,12 +214,9 @@ export default defineComponent({ const isSrcTableSet = () => { return ( - props.sourceTable !== "" && - props.sourceFolder !== "" && - props.sourceProject !== "" && - props.sourceTable !== undefined && - props.sourceFolder !== undefined && - props.sourceProject !== undefined + !isEmpty(props.sourceTable) && + !isEmpty(props.sourceFolder) && + !isEmpty(props.sourceProject) ); }; const fetchVariables = async () => { @@ -235,6 +243,7 @@ export default defineComponent({ data(): ViewEditorData { return { projectData: {}, + formValidated: false, vwTable: this.viewTable ? this.viewTable : "", vwProject: this.viewProject ? this.viewProject : "", vwFolder: this.viewFolder ? this.viewFolder : "", @@ -245,6 +254,7 @@ export default defineComponent({ }, methods: { getTablesFromListOfFiles, + isEmpty, async getProjectContent(project: string) { await getProject(project) .then((data) => { @@ -257,7 +267,6 @@ export default defineComponent({ }); }, async getVariables(project: string, folder: string, file: string) { - console.log(project, folder, file); await getTableVariables(project, folder + "%2F" + file) .then((response) => { this.variables = response; @@ -269,17 +278,36 @@ export default defineComponent({ }); }, updateSrcProject(event: Event) { + this.formValidated = false; this.srcProject = event.toString(); }, updateVwProject(event: Event) { this.vwProject = event.toString(); }, updateSrcFolder(event: Event) { + this.formValidated = false; this.srcFolder = event.toString(); }, updateSrcTable(event: Event) { + this.formValidated = false; this.srcTable = event.toString(); }, + getSelectedVariables() { + return this.$refs.variableSelector && (this.$refs.variableSelector as any).selectedVariables ? (this.$refs.variableSelector as any).selectedVariables : []; + }, + saveIfValid(fileInfoSet: boolean, selectedVariables: StringArray) { + if(fileInfoSet && !isEmpty(selectedVariables)){ + this.onSave( + this.srcProject, + this.sourceObject, + this.vwProject, + this.linkedObject, + selectedVariables + ) + } else { + this.formValidated = true; + } + } }, watch: { srcProject() { @@ -294,6 +322,16 @@ export default defineComponent({ }, }, computed: { + allFileInformationProvided(): boolean { + return ( + !isEmpty(this.vwTable) && + !isEmpty(this.vwFolder) && + !isEmpty(this.vwProject) && + !isEmpty(this.srcProject) && + !isEmpty(this.srcFolder) && + !isEmpty(this.srcTable)); + + }, linkedObject(): string { return `${this.vwFolder}/${this.vwTable}`; }, @@ -303,15 +341,15 @@ export default defineComponent({ isEditMode(): boolean { // when all items are preselected, we are in edit mode return ( - this.sourceFolder !== undefined && - this.sourceProject !== undefined && - this.sourceTable !== undefined && - this.viewFolder !== undefined && - this.viewProject !== undefined && - this.viewTable !== undefined && - this.preselectedVariables.length > 0 + !isEmpty(this.sourceFolder) && + !isEmpty(this.sourceProject) && + !isEmpty(this.sourceTable) && + !isEmpty(this.viewFolder) && + !isEmpty(this.viewProject) && + !isEmpty(this.viewTable) && + !isEmpty(this.preselectedVariables) ); }, }, }); - + \ No newline at end of file diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index 7aa9bd5ea..9c4278ff2 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -219,6 +219,21 @@ export function diskSpaceBelowThreshold(diskSpace: number): boolean { return diskSpace < 2147483648; } +export function isEmpty(variable: any): boolean { + // function will return true if empty string, empty object or empty array, else false + if (variable === undefined || variable === null || variable === '') { + return true; + } else if(typeof(variable) === 'object') { + if (Array.isArray(variable)) { + return variable.length === 0; + } else { + return isEmptyObject(variable); + } + } else { + return false; + } +} + /** * Convert given bytes to 2 digits precision round exponent version string. * @param bytes number diff --git a/ui/src/types/types.d.ts b/ui/src/types/types.d.ts index 8cbd11beb..f2e8c9d2d 100644 --- a/ui/src/types/types.d.ts +++ b/ui/src/types/types.d.ts @@ -1,3 +1,4 @@ + import { Project, User } from "./api"; export type StringObject = Record>; @@ -128,4 +129,5 @@ export type ViewEditorData = { srcTable: string; srcProject: string; srcFolder: string; + formValidated: boolean; }; diff --git a/ui/src/views/ProjectsExplorer.vue b/ui/src/views/ProjectsExplorer.vue index e604e120e..496327735 100644 --- a/ui/src/views/ProjectsExplorer.vue +++ b/ui/src/views/ProjectsExplorer.vue @@ -341,25 +341,7 @@ export default defineComponent({ this.loading_preview = false; }); - getFileDetails( - this.projectId, - `${this.selectedObject}` - ) - .then((data) => { - this.fileInfo.fileSize = data["size"]; - this.fileInfo.dataSizeRows = parseInt(data["rows"]); - this.fileInfo.dataSizeColumns = parseInt(data["columns"]); - this.fileInfo.sourceLink = data["sourceLink"]; - this.fileInfo.variables = data["variables"]; - if (isLinkFileType(this.selectedFile)) { - this.columnNames = this.fileInfo.variables - } else { - this.getTableColumnNames( this.projectId, `${this.selectedObject}`) - } - }) - .catch((error) => { - this.errorMessage = `Cannot load details for [${this.selectedObject}] of project [${this.projectId}]. Because: ${error}.`; - }); + this.setFileDetails(); } } }, @@ -383,10 +365,32 @@ export default defineComponent({ isTableType, isLinkFileType, isNonTableType, + setFileDetails() { + getFileDetails( + this.projectId, + `${this.selectedObject}` + ) + .then((data) => { + this.fileInfo.fileSize = data["size"]; + this.fileInfo.dataSizeRows = parseInt(data["rows"]); + this.fileInfo.dataSizeColumns = parseInt(data["columns"]); + this.fileInfo.sourceLink = data["sourceLink"]; + this.fileInfo.variables = data["variables"]; + if (isLinkFileType(this.selectedFile)) { + this.columnNames = this.fileInfo.variables + } else { + this.setTableColumnNames( this.projectId, `${this.selectedObject}`) + } + }) + .catch((error) => { + this.errorMessage = `Cannot load details for [${this.selectedObject}] of project [${this.projectId}]. Because: ${error}.`; + }); + }, askIfPreviewIsEmpty() { return isEmptyObject(this.filePreview[0]); }, cancelView() { + this.setFileDetails(); this.createLinkFromSrc = false; this.editView = false; }, @@ -420,7 +424,7 @@ export default defineComponent({ this.errorMessage = "Folder name cannot be empty"; } }, - getTableColumnNames (project: string, object: string) { + setTableColumnNames (project: string, object: string) { getTableVariables( project, object diff --git a/ui/tests/unit/components/FormValidation.spec.ts b/ui/tests/unit/components/FormValidation.spec.ts new file mode 100644 index 000000000..d053f4a99 --- /dev/null +++ b/ui/tests/unit/components/FormValidation.spec.ts @@ -0,0 +1,31 @@ +import { mount, VueWrapper } from "@vue/test-utils"; +import FormValidation from "@/components/FormValidation.vue"; + +describe("ButtonGroup", () => { + const msg = "Invalid form! Please fix."; + const mockFunction = jest.fn(); + let wrapper: VueWrapper; + beforeEach(function () { + wrapper = mount(FormValidation, { + props: { + validationCondition: false, + invalidMessage: msg + }, + }); + }); + + test("validation message not shown when isValidated not defined", () => { + expect(wrapper.html()).not.toContain(msg) + }); + + test("validation message not shown when isValidated true and condition false", async () => { + await wrapper.setProps({ isValidated: true}); + expect(wrapper.html()).not.toContain(msg) + }); + + test("validation message shown when isValidated true and condition false", async () => { + await wrapper.setProps({ validationCondition: true, isValidated: true}); + expect(wrapper.html()).toContain(msg) + }); + +}); diff --git a/ui/tests/unit/components/ViewEditor.spec.ts b/ui/tests/unit/components/ViewEditor.spec.ts index 674ef14c0..e2c47a8a8 100644 --- a/ui/tests/unit/components/ViewEditor.spec.ts +++ b/ui/tests/unit/components/ViewEditor.spec.ts @@ -3,10 +3,11 @@ import ViewEditor from "@/components/ViewEditor.vue"; describe("ViewEditor", () => { let wrapper: VueWrapper; + const testFunction = jest.fn() beforeEach(function () { wrapper = shallowMount(ViewEditor, { props: { - + onSave: testFunction }, }); }); @@ -22,4 +23,134 @@ describe("ViewEditor", () => { expect(wrapper.vm.sourceObject).toBe("my-folder/my-table"); }); + test("saveIfValid doesnt save if file info data not set", () => { + wrapper.vm.saveIfValid(false, ["a", "b", "c"]); + expect(wrapper.vm.formValidated).toBe(true); + expect(testFunction).not.toHaveBeenCalled(); + }) + + test("saveIfValid doesnt save if variables not set", () => { + wrapper.vm.saveIfValid(true, []); + expect(wrapper.vm.formValidated).toBe(true); + expect(testFunction).not.toHaveBeenCalled(); + }) + + test("saveIfValid does save if all view data set", () => { + const srcProject = "project"; + const srcFolder = "folder"; + const srcTable = "table"; + const vwProject = "link-project"; + const vwFolder = "link-folder"; + const vwTable = "filename"; + const variables = ["a", "b", "c"]; + wrapper.vm.srcProject = srcProject; + wrapper.vm.srcFolder = srcFolder; + wrapper.vm.srcTable = srcTable; + wrapper.vm.vwProject = vwProject; + wrapper.vm.vwFolder = vwFolder; + wrapper.vm.vwTable = vwTable; + wrapper.vm.saveIfValid(true, variables); + expect(testFunction).toHaveBeenCalledWith(srcProject, srcFolder+"/"+ srcTable, vwProject,vwFolder+"/"+ vwTable, variables); + }) + + test("allFileInformationProvided true when all projects, folders and tables set", () => { + const srcProject = "project"; + const srcFolder = "folder"; + const srcTable = "table"; + const vwProject = "link-project"; + const vwFolder = "link-folder"; + const vwTable = "filename"; + wrapper.vm.srcProject = srcProject; + wrapper.vm.srcFolder = srcFolder; + wrapper.vm.srcTable = srcTable; + wrapper.vm.vwProject = vwProject; + wrapper.vm.vwFolder = vwFolder; + wrapper.vm.vwTable = vwTable; + expect(wrapper.vm.allFileInformationProvided).toBe(true); + }) + + test("allFileInformationProvided false when all srcProject not set", () => { + const srcFolder = "folder"; + const srcTable = "table"; + const vwProject = "link-project"; + const vwFolder = "link-folder"; + const vwTable = "filename"; + wrapper.vm.srcFolder = srcFolder; + wrapper.vm.srcTable = srcTable; + wrapper.vm.vwProject = vwProject; + wrapper.vm.vwFolder = vwFolder; + wrapper.vm.vwTable = vwTable; + expect(wrapper.vm.allFileInformationProvided).toBe(false); + }) + + test("allFileInformationProvided false when all srcFolder not set", () => { + const srcProject = "project"; + const srcTable = "table"; + const vwProject = "link-project"; + const vwFolder = "link-folder"; + const vwTable = "filename"; + wrapper.vm.srcProject = srcProject; + wrapper.vm.srcTable = srcTable; + wrapper.vm.vwProject = vwProject; + wrapper.vm.vwFolder = vwFolder; + wrapper.vm.vwTable = vwTable; + expect(wrapper.vm.allFileInformationProvided).toBe(false); + }) + + test("allFileInformationProvided false when all srcTable not set", () => { + const srcProject = "project"; + const srcFolder = "folder"; + const vwProject = "link-project"; + const vwFolder = "link-folder"; + const vwTable = "filename"; + wrapper.vm.srcProject = srcProject; + wrapper.vm.srcFolder = srcFolder; + wrapper.vm.vwProject = vwProject; + wrapper.vm.vwFolder = vwFolder; + wrapper.vm.vwTable = vwTable; + expect(wrapper.vm.allFileInformationProvided).toBe(false); + }) + + test("allFileInformationProvided false when all vwProject not set", () => { + const srcProject = "project"; + const srcFolder = "folder"; + const srcTable = "table"; + const vwFolder = "link-folder"; + const vwTable = "filename"; + wrapper.vm.srcProject = srcProject; + wrapper.vm.srcFolder = srcFolder; + wrapper.vm.srcTable = srcTable; + wrapper.vm.vwFolder = vwFolder; + wrapper.vm.vwTable = vwTable; + expect(wrapper.vm.allFileInformationProvided).toBe(false); + }) + + test("allFileInformationProvided false when all vwFolder not set", () => { + const srcProject = "project"; + const srcFolder = "folder"; + const srcTable = "table"; + const vwProject = "link-project"; + const vwTable = "filename"; + wrapper.vm.srcProject = srcProject; + wrapper.vm.srcFolder = srcFolder; + wrapper.vm.srcTable = srcTable; + wrapper.vm.vwProject = vwProject; + wrapper.vm.vwTable = vwTable; + expect(wrapper.vm.allFileInformationProvided).toBe(false); + }) + + test("allFileInformationProvided false when all vwTable not set", () => { + const srcProject = "project"; + const srcFolder = "folder"; + const srcTable = "table"; + const vwProject = "link-project"; + const vwFolder = "link-folder"; + wrapper.vm.srcProject = srcProject; + wrapper.vm.srcFolder = srcFolder; + wrapper.vm.srcTable = srcTable; + wrapper.vm.vwProject = vwProject; + wrapper.vm.vwFolder = vwFolder; + expect(wrapper.vm.allFileInformationProvided).toBe(false); + }) }); + \ No newline at end of file diff --git a/ui/tests/unit/helpers/utils.spec.ts b/ui/tests/unit/helpers/utils.spec.ts index 4088ef5fe..e2ac0a9bc 100644 --- a/ui/tests/unit/helpers/utils.spec.ts +++ b/ui/tests/unit/helpers/utils.spec.ts @@ -18,7 +18,8 @@ import { isLinkFileType, encodeUriComponent, convertBytes, - diskSpaceBelowThreshold + diskSpaceBelowThreshold, + isEmpty } from "@/helpers/utils"; import { StringObject } from "@/types/types"; @@ -289,7 +290,6 @@ describe("utils", () => { expect(actual).toEqual("project%2Did%2Ffolder%2Ffile%2Dversion1.parquet"); }); }); -}); describe("convertBytes", () => { it("bytes", () => { @@ -329,6 +329,53 @@ describe("utils", () => { expect(actual).toEqual(false); }); }); - + describe("isEmpty", () => { + it("Returns true when undefined", () => { + const actual = isEmpty(undefined); + expect(actual).toEqual(true); + }); + + it("Returns true when null", () => { + const actual = isEmpty(null); + expect(actual).toEqual(true); + }); + + it("Returns true when empty string", () => { + const actual = isEmpty(''); + expect(actual).toEqual(true); + }); + + it("Returns true when empty array", () => { + const actual = isEmpty([]); + expect(actual).toEqual(true); + }); + + it("Returns true when empty object", () => { + const actual = isEmpty({}); + expect(actual).toEqual(true); + }); + + it("Returns false when array with element", () => { + const actual = isEmpty(['element']); + expect(actual).toEqual(false); + }); + + it("Returns false when object with element", () => { + const actual = isEmpty({"el": undefined}); + expect(actual).toEqual(false); + }); + + it("Returns false when string with content", () => { + const actual = isEmpty("el"); + expect(actual).toEqual(false); + }); + + it("Returns false when number", () => { + const actual = isEmpty(0); + expect(actual).toEqual(false); + }); + }); +}); + \ No newline at end of file